---
.../java/WebsiteManifestGenerator.java | 273 ++++++++++++++++++
.../WebsiteManifestGeneratorTest.java | 40 ++-
2 files changed, 292 insertions(+), 21 deletions(-)
create mode 100644 dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
new file mode 100644
index 0000000000000..fc0991244718b
--- /dev/null
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * JDK-only tool that computes SHA-256 checksums for a release TAR/ZIP pair and writes the
+ * website's four-field release manifest ({@code format}, {@code version}, {@code tar_sha256},
+ * {@code zip_sha256}). Per-version manifests are immutable; {@code latest.properties} can only
+ * move forward to a higher semantic version and cannot silently change checksums for the same
+ * version. Writes are atomic.
+ *
+ *
+ * Usage: {@code java WebsiteManifestGenerator.java --version X.Y.Z --tar --zip
+ * --output --latest }
+ */
+public class WebsiteManifestGenerator {
+
+ private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$");
+ private static final Set REQUIRED_OPTIONS
+ = new LinkedHashSet<>(java.util.List.of("--version", "--tar", "--zip", "--output", "--latest"));
+ private static final String MANIFEST_FORMAT = "1";
+
+ public static void main(String[] args) {
+ try {
+ run(args);
+ } catch (UsageException e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(2);
+ } catch (ConflictException e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(1);
+ } catch (IOException e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(1);
+ }
+ }
+
+ private static void run(String[] args) throws IOException {
+ Map options = parseArgs(args);
+
+ String version = options.get("--version");
+ if (!VERSION_PATTERN.matcher(version).matches()) {
+ throw new UsageException("'" + version + "' is not a valid release version (expected X.Y.Z).");
+ }
+
+ Path tar = Paths.get(options.get("--tar"));
+ Path zip = Paths.get(options.get("--zip"));
+ if (!Files.isRegularFile(tar)) {
+ throw new UsageException("TAR artifact not found: " + tar);
+ }
+ if (!Files.isRegularFile(zip)) {
+ throw new UsageException("ZIP artifact not found: " + zip);
+ }
+
+ String latestValue = options.get("--latest");
+ boolean latest;
+ if ("true".equals(latestValue)) {
+ latest = true;
+ } else if ("false".equals(latestValue)) {
+ latest = false;
+ } else {
+ throw new UsageException("--latest must be 'true' or 'false' (got '" + latestValue + "').");
+ }
+
+ Path output = Paths.get(options.get("--output"));
+
+ String tarSha256 = sha256Hex(tar);
+ String zipSha256 = sha256Hex(zip);
+ byte[] manifest = renderManifest(version, tarSha256, zipSha256);
+
+ Path releasesDir = output.resolve("releases");
+ Files.createDirectories(releasesDir);
+ writeVersionManifest(releasesDir.resolve(version + ".properties"), manifest, version);
+
+ if (latest) {
+ writeLatestManifest(output.resolve("latest.properties"), manifest, version);
+ }
+ }
+
+ private static Map parseArgs(String[] args) {
+ Map options = new LinkedHashMap<>();
+ int i = 0;
+ while (i < args.length) {
+ String key = args[i];
+ if (!REQUIRED_OPTIONS.contains(key)) {
+ throw new UsageException("unknown option '" + key + "'.");
+ }
+ if (i + 1 >= args.length) {
+ throw new UsageException("option '" + key + "' requires a value.");
+ }
+ if (options.containsKey(key)) {
+ throw new UsageException("option '" + key + "' was specified more than once.");
+ }
+ options.put(key, args[i + 1]);
+ i += 2;
+ }
+ for (String required : REQUIRED_OPTIONS) {
+ if (!options.containsKey(required)) {
+ throw new UsageException("missing required option '" + required + "'.");
+ }
+ }
+ return options;
+ }
+
+ private static byte[] renderManifest(String version, String tarSha256, String zipSha256) {
+ String content = "format=" + MANIFEST_FORMAT + "\n"
+ + "version=" + version + "\n"
+ + "tar_sha256=" + tarSha256 + "\n"
+ + "zip_sha256=" + zipSha256 + "\n";
+ return content.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static void writeVersionManifest(Path versionFile, byte[] manifest, String version) throws IOException {
+ if (Files.exists(versionFile)) {
+ byte[] existing = Files.readAllBytes(versionFile);
+ if (java.util.Arrays.equals(existing, manifest)) {
+ return;
+ }
+ throw new ConflictException("version manifest for " + version + " already exists with different content"
+ + " (" + versionFile + ") - version manifests are immutable.");
+ }
+ atomicWrite(versionFile, manifest);
+ }
+
+ private static void writeLatestManifest(Path latestFile, byte[] manifest, String version) throws IOException {
+ if (!Files.exists(latestFile)) {
+ atomicWrite(latestFile, manifest);
+ return;
+ }
+
+ byte[] existing = Files.readAllBytes(latestFile);
+ if (java.util.Arrays.equals(existing, manifest)) {
+ return;
+ }
+
+ Map existingFields = parseStrictManifest(existing, latestFile);
+ String existingVersion = existingFields.get("version");
+ int cmp = compareSemver(version, existingVersion);
+ if (cmp < 0) {
+ throw new ConflictException(
+ "latest.properties already points at a newer version (" + existingVersion + "); refusing to"
+ + " move backward to " + version + ".");
+ } else if (cmp == 0) {
+ throw new ConflictException("latest.properties already has different checksums for version " + version
+ + " - refusing to overwrite.");
+ }
+ atomicWrite(latestFile, manifest);
+ }
+
+ private static Map parseStrictManifest(byte[] bytes, Path source) {
+ String content = new String(bytes, StandardCharsets.UTF_8);
+ Map fields = new LinkedHashMap<>();
+ for (String line : content.split("\n", -1)) {
+ if (line.isEmpty()) {
+ continue;
+ }
+ int eq = line.indexOf('=');
+ if (eq < 0) {
+ throw new ConflictException("malformed manifest line in " + source + ": '" + line + "'.");
+ }
+ fields.put(line.substring(0, eq), line.substring(eq + 1));
+ }
+ Set expectedKeys = Set.of("format", "version", "tar_sha256", "zip_sha256");
+ if (!fields.keySet().equals(expectedKeys)) {
+ throw new ConflictException("malformed manifest in " + source + ": expected keys " + expectedKeys
+ + " but found " + fields.keySet() + ".");
+ }
+ if (!VERSION_PATTERN.matcher(fields.get("version")).matches()) {
+ throw new ConflictException("malformed manifest in " + source + ": invalid version '"
+ + fields.get("version") + "'.");
+ }
+ return fields;
+ }
+
+ private static int compareSemver(String a, String b) {
+ int[] pa = semverParts(a);
+ int[] pb = semverParts(b);
+ for (int i = 0; i < 3; i++) {
+ int c = Integer.compare(pa[i], pb[i]);
+ if (c != 0) {
+ return c;
+ }
+ }
+ return 0;
+ }
+
+ private static int[] semverParts(String version) {
+ java.util.regex.Matcher m = VERSION_PATTERN.matcher(version);
+ if (!m.matches()) {
+ throw new ConflictException("invalid semantic version '" + version + "'.");
+ }
+ return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)) };
+ }
+
+ private static void atomicWrite(Path target, byte[] bytes) throws IOException {
+ Path dir = target.toAbsolutePath().getParent();
+ Files.createDirectories(dir);
+ Path tmp = Files.createTempFile(dir, "wmg-", ".tmp");
+ try {
+ Files.write(tmp, bytes);
+ try {
+ Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
+ }
+ } finally {
+ Files.deleteIfExists(tmp);
+ }
+ }
+
+ private static String sha256Hex(Path file) throws IOException {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is required by the JDK and must always be available.", e);
+ }
+ try (InputStream in = Files.newInputStream(file)) {
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ digest.update(buffer, 0, read);
+ }
+ }
+ byte[] hash = digest.digest();
+ StringBuilder sb = new StringBuilder(hash.length * 2);
+ for (byte b : hash) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ private static final class UsageException extends RuntimeException {
+ UsageException(String message) {
+ super(message);
+ }
+ }
+
+ private static final class ConflictException extends RuntimeException {
+ ConflictException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteManifestGeneratorTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteManifestGeneratorTest.java
index 5bf486de140cf..eb5a503453876 100644
--- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteManifestGeneratorTest.java
+++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteManifestGeneratorTest.java
@@ -190,33 +190,31 @@ void latestRollbackFails(@TempDir Path work) throws Exception {
@Test
void sameVersionChecksumConflictOnLatestFails(@TempDir Path work) throws Exception {
- Path tar1 = writeFixture(work, "x.tar.gz", "x-tar");
- Path zip1 = writeFixture(work, "x.zip", "x-zip");
Path output = work.resolve("website");
-
- Result seed = run("--version", "4.22.0", "--tar", tar1.toString(), "--zip", zip1.toString(), "--output",
- output.toString(), "--latest", "true");
- assertEquals(0, seed.exit, seed.stderr);
- byte[] latestBefore = Files.readAllBytes(output.resolve("latest.properties"));
-
- // Simulate a corrupted/foreign latest.properties: same version, different checksums, with no
- // matching releases/4.22.0.properties conflict (different version so the version-file step succeeds).
- Path tar2 = writeFixture(work, "y.tar.gz", "y-tar-different");
- Path zip2 = writeFixture(work, "y.zip", "y-zip-different");
- Files.writeString(output.resolve("latest.properties"),
- expectedManifest("4.22.0", sha256Hex(tar2), sha256Hex(zip2)), StandardCharsets.UTF_8);
- byte[] tamperedLatest = Files.readAllBytes(output.resolve("latest.properties"));
-
- Path tar3 = writeFixture(work, "z.tar.gz", "z-tar-yet-different");
- Path zip3 = writeFixture(work, "z.zip", "z-zip-yet-different");
-
- Result r = run("--version", "4.22.0", "--tar", tar3.toString(), "--zip", zip3.toString(), "--output",
+ Files.createDirectories(output);
+
+ // Seed a latest.properties directly (bypassing the generator) that claims version 4.22.0 with
+ // checksums that do not correspond to any releases/4.22.0.properties file yet. This simulates
+ // stale/foreign state: the releases/ manifest for that version does not exist, so the
+ // version-file write below succeeds fresh, isolating the latest-specific conflict check.
+ Path tarStale = writeFixture(work, "stale.tar.gz", "stale-tar");
+ Path zipStale = writeFixture(work, "stale.zip", "stale-zip");
+ byte[] tamperedLatest
+ = expectedManifest("4.22.0", sha256Hex(tarStale), sha256Hex(zipStale)).getBytes(StandardCharsets.UTF_8);
+ Files.write(output.resolve("latest.properties"), tamperedLatest);
+ assertFalse(Files.exists(output.resolve("releases").resolve("4.22.0.properties")));
+
+ Path tarNew = writeFixture(work, "new.tar.gz", "new-tar-different");
+ Path zipNew = writeFixture(work, "new.zip", "new-zip-different");
+
+ Result r = run("--version", "4.22.0", "--tar", tarNew.toString(), "--zip", zipNew.toString(), "--output",
output.toString(), "--latest", "true");
assertNotEquals(0, r.exit, "same-version checksum conflict against latest.properties must fail");
assertArrayEquals(tamperedLatest, Files.readAllBytes(output.resolve("latest.properties")),
"latest.properties must remain untouched after a rejected conflicting update");
- assertNotEquals(new String(latestBefore, StandardCharsets.UTF_8), "unused");
+ assertTrue(Files.exists(output.resolve("releases").resolve("4.22.0.properties")),
+ "the version-specific manifest still gets written since it did not previously exist");
}
@Test
From abb139cefe583327841a6c8c424f4f1e1e48849c Mon Sep 17 00:00:00 2001
From: Adriano Machado <60320+ammachado@users.noreply.github.com>
Date: Sun, 12 Jul 2026 21:25:43 -0400
Subject: [PATCH 12/16] CAMEL-23703: camel-launcher - prepare website installer
output
Also fixes a MODULE_DIR path bug in both wrappers: it resolved two
directory levels up from src/jreleaser/bin instead of three, so it
pointed at .../camel-launcher/src instead of the module root. This was
latent since Task 4 because --print-plan never touched MODULE_DIR.
Co-Authored-By: Claude Sonnet 5
---
.../src/jreleaser/bin/camel-package.bat | 94 ++++++++--
.../src/jreleaser/bin/camel-package.sh | 72 +++++++-
.../jbang/launcher/PackagePlanBatTest.java | 171 ++++++++++++++++-
.../dsl/jbang/launcher/PackagePlanShTest.java | 173 +++++++++++++++++-
4 files changed, 487 insertions(+), 23 deletions(-)
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
index a3fbaaa686821..142e92ff00158 100644
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
@@ -1,20 +1,3 @@
-@REM
-@REM Licensed to the Apache Software Foundation (ASF) under one or more
-@REM contributor license agreements. See the NOTICE file distributed with
-@REM this work for additional information regarding copyright ownership.
-@REM The ASF licenses this file to You under the Apache License, Version 2.0
-@REM (the "License"); you may not use this file except in compliance with
-@REM the License. You may obtain a copy of the License at
-@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing, software
-@REM distributed under the License is distributed on an "AS IS" BASIS,
-@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@REM See the License for the specific language governing permissions and
-@REM limitations under the License.
-@REM
-
@echo off
@REM
@REM Licensed to the Apache Software Foundation (ASF) under one or more
@@ -118,8 +101,83 @@ if /I "%SUBCOMMAND%"=="publish" (
exit /b 2
)
+set "MODULE_DIR=%SCRIPT_DIR%..\..\.."
+
+@REM Resolve the release version. Production always reads Maven's project.version; tests/CI may
+@REM override it, but only with both CAMEL_PACKAGE_TEST_MODE=true and CAMEL_PACKAGE_TEST_VERSION
+@REM set, so production can never accidentally skip the real Maven version.
+if not "%CAMEL_PACKAGE_TEST_VERSION%"=="" (
+ if not "%CAMEL_PACKAGE_TEST_MODE%"=="true" (
+ echo Error: CAMEL_PACKAGE_TEST_VERSION requires CAMEL_PACKAGE_TEST_MODE=true. 1>&2
+ exit /b 2
+ )
+ set "PROJECT_VERSION=%CAMEL_PACKAGE_TEST_VERSION%"
+) else (
+ for /f "usebackq delims=" %%v in (`mvn -q -B -ntp -pl "%MODULE_DIR%" org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate -Dexpression=project.version -DforceStdout`) do set "PROJECT_VERSION=%%v"
+)
+
+if "!PROJECT_VERSION:~-8!"=="SNAPSHOT" (
+ echo Error: refusing to prepare packages for a snapshot version '!PROJECT_VERSION!'. 1>&2
+ exit /b 2
+)
+
+set "TAR=%MODULE_DIR%\target\camel-launcher-!PROJECT_VERSION!-bin.tar.gz"
+set "ZIP=%MODULE_DIR%\target\camel-launcher-!PROJECT_VERSION!-bin.zip"
+set "INSTALL_SH_SRC=%MODULE_DIR%\src\install\install.sh"
+set "INSTALL_PS1_SRC=%MODULE_DIR%\src\install\install.ps1"
+
+if not exist "!TAR!" (
+ echo Error: release TAR not found: !TAR! 1>&2
+ exit /b 1
+)
+if not exist "!ZIP!" (
+ echo Error: release ZIP not found: !ZIP! 1>&2
+ exit /b 1
+)
+if not exist "!INSTALL_SH_SRC!" (
+ echo Error: installer source not found: !INSTALL_SH_SRC! 1>&2
+ exit /b 1
+)
+if not exist "!INSTALL_PS1_SRC!" (
+ echo Error: installer source not found: !INSTALL_PS1_SRC! 1>&2
+ exit /b 1
+)
+
+@REM Recreate only the prepared website staging directory (leave the rest of target\jreleaser alone).
+set "WEBSITE_DIR=%MODULE_DIR%\target\jreleaser\website"
+if exist "!WEBSITE_DIR!" rd /s /q "!WEBSITE_DIR!"
+mkdir "!WEBSITE_DIR!"
+
+copy /y "!INSTALL_SH_SRC!" "!WEBSITE_DIR!\install.sh" >nul
+if errorlevel 1 (
+ echo Error: failed to copy install.sh. 1>&2
+ exit /b 1
+)
+copy /y "!INSTALL_PS1_SRC!" "!WEBSITE_DIR!\install.ps1" >nul
+if errorlevel 1 (
+ echo Error: failed to copy install.ps1. 1>&2
+ exit /b 1
+)
+
+fc /b "!INSTALL_SH_SRC!" "!WEBSITE_DIR!\install.sh" >nul
+if errorlevel 1 (
+ echo Error: copied install.sh does not match its source. 1>&2
+ exit /b 1
+)
+fc /b "!INSTALL_PS1_SRC!" "!WEBSITE_DIR!\install.ps1" >nul
+if errorlevel 1 (
+ echo Error: copied install.ps1 does not match its source. 1>&2
+ exit /b 1
+)
+
+call java "%MODULE_DIR%\src\jreleaser\java\WebsiteManifestGenerator.java" --version !PROJECT_VERSION! --tar "!TAR!" --zip "!ZIP!" --output "!WEBSITE_DIR!\camel-cli" --latest !WEBSITE_LATEST!
+if errorlevel 1 (
+ echo Error: website manifest generation failed. 1>&2
+ exit /b 1
+)
+
echo Preparing packages for channel '%CHANNEL%' ^(packagers: !PACKAGERS!^)...
-call mvn -B -ntp -pl "%SCRIPT_DIR%..\.." -Dcamel.pkg.channel=%CHANNEL% -Dcamel.pkg.ltsLine=%LTS_LINE% -Dcamel.pkg.packagers=!PACKAGERS! -Dcamel.pkg.brewFormula=!BREW_FORMULA! -Dcamel.pkg.brewLtsFormula=!BREW_LTS_FORMULA! -Dcamel.pkg.sdkmanDefault=!SDKMAN_DEFAULT! jreleaser:config jreleaser:prepare jreleaser:package -Djreleaser.dry.run=true
+call mvn -B -ntp -pl "%MODULE_DIR%" -Dcamel.pkg.channel=%CHANNEL% -Dcamel.pkg.ltsLine=%LTS_LINE% -Dcamel.pkg.packagers=!PACKAGERS! -Dcamel.pkg.brewFormula=!BREW_FORMULA! -Dcamel.pkg.brewLtsFormula=!BREW_LTS_FORMULA! -Dcamel.pkg.sdkmanDefault=!SDKMAN_DEFAULT! jreleaser:config jreleaser:prepare jreleaser:package -Djreleaser.dry.run=true
exit /b %ERRORLEVEL%
:usage
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
index c4e66bcf31f83..ee0afb5bdbaae 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
@@ -19,7 +19,7 @@
set -eu
SCRIPT_DIR=`CDPATH= cd -- "$(dirname -- "$0")" && pwd`
-MODULE_DIR=`CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd`
+MODULE_DIR=`CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd`
SUPPORTED_LTS="$SCRIPT_DIR/../supported-lts.yml"
usage() {
@@ -112,6 +112,76 @@ if [ "$SUBCOMMAND" = "publish" ]; then
fi
# --- prepare: no remote mutation, no credentials ---
+
+# Resolve the release version. Production always reads Maven's project.version; tests/CI may
+# override it, but only with both CAMEL_PACKAGE_TEST_MODE=true and CAMEL_PACKAGE_TEST_VERSION set,
+# so production can never accidentally skip the real Maven version.
+if [ -n "${CAMEL_PACKAGE_TEST_VERSION:-}" ]; then
+ if [ "${CAMEL_PACKAGE_TEST_MODE:-}" != "true" ]; then
+ echo "Error: CAMEL_PACKAGE_TEST_VERSION requires CAMEL_PACKAGE_TEST_MODE=true." 1>&2
+ exit 2
+ fi
+ PROJECT_VERSION="$CAMEL_PACKAGE_TEST_VERSION"
+else
+ PROJECT_VERSION=`mvn -q -B -ntp -pl "$MODULE_DIR" org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \
+ -Dexpression=project.version -DforceStdout`
+fi
+
+case "$PROJECT_VERSION" in
+ *-SNAPSHOT)
+ echo "Error: refusing to prepare packages for a snapshot version '$PROJECT_VERSION'." 1>&2
+ exit 2
+ ;;
+esac
+
+# Locate the release artifacts and canonical website installer sources.
+TAR="$MODULE_DIR/target/camel-launcher-$PROJECT_VERSION-bin.tar.gz"
+ZIP="$MODULE_DIR/target/camel-launcher-$PROJECT_VERSION-bin.zip"
+INSTALL_SH_SRC="$MODULE_DIR/src/install/install.sh"
+INSTALL_PS1_SRC="$MODULE_DIR/src/install/install.ps1"
+
+if [ ! -f "$TAR" ]; then
+ echo "Error: release TAR not found: $TAR" 1>&2
+ exit 1
+fi
+if [ ! -f "$ZIP" ]; then
+ echo "Error: release ZIP not found: $ZIP" 1>&2
+ exit 1
+fi
+if [ ! -f "$INSTALL_SH_SRC" ]; then
+ echo "Error: installer source not found: $INSTALL_SH_SRC" 1>&2
+ exit 1
+fi
+if [ ! -f "$INSTALL_PS1_SRC" ]; then
+ echo "Error: installer source not found: $INSTALL_PS1_SRC" 1>&2
+ exit 1
+fi
+
+# Recreate only the prepared website staging directory (leave the rest of target/jreleaser alone).
+WEBSITE_DIR="$MODULE_DIR/target/jreleaser/website"
+rm -rf "$WEBSITE_DIR"
+mkdir -p "$WEBSITE_DIR"
+
+cp -p "$INSTALL_SH_SRC" "$WEBSITE_DIR/install.sh"
+cp -p "$INSTALL_PS1_SRC" "$WEBSITE_DIR/install.ps1"
+
+if ! cmp -s "$INSTALL_SH_SRC" "$WEBSITE_DIR/install.sh"; then
+ echo "Error: copied install.sh does not match its source." 1>&2
+ exit 1
+fi
+if ! cmp -s "$INSTALL_PS1_SRC" "$WEBSITE_DIR/install.ps1"; then
+ echo "Error: copied install.ps1 does not match its source." 1>&2
+ exit 1
+fi
+
+if ! java "$MODULE_DIR/src/jreleaser/java/WebsiteManifestGenerator.java" \
+ --version "$PROJECT_VERSION" --tar "$TAR" --zip "$ZIP" \
+ --output "$WEBSITE_DIR/camel-cli" \
+ --latest "$WEBSITE_LATEST"; then
+ echo "Error: website manifest generation failed." 1>&2
+ exit 1
+fi
+
# Map the plan to JReleaser system properties. Actual property names are validated
# against the JReleaser 1.25.0 reference during template work (Tasks 7-9).
echo "Preparing packages for channel '$CHANNEL' (packagers: $PACKAGERS)..."
diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanBatTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanBatTest.java
index b7abd15988e85..ed5831965dd0e 100644
--- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanBatTest.java
+++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanBatTest.java
@@ -16,19 +16,29 @@
*/
package org.apache.camel.dsl.jbang.launcher;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.security.MessageDigest;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -39,6 +49,10 @@
class PackagePlanBatTest {
private static final Path SCRIPT = Paths.get("src/jreleaser/bin/camel-package.bat");
+ private static final Path MODULE_DIR = Paths.get("").toAbsolutePath();
+
+ /** Fixture release version used by the website-staging integration tests. Never a real release. */
+ private static final String TEST_VERSION = "9.9.9";
private static final class Result {
int exit;
@@ -47,6 +61,10 @@ private static final class Result {
}
private Result run(String... args) throws Exception {
+ return run(Map.of(), args);
+ }
+
+ private Result run(Map extraEnv, String... args) throws Exception {
List cmd = new ArrayList<>();
cmd.add("cmd.exe");
cmd.add("/c");
@@ -54,10 +72,12 @@ private Result run(String... args) throws Exception {
for (String a : args) {
cmd.add(a);
}
- Process p = new ProcessBuilder(cmd).start();
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.environment().putAll(extraEnv);
+ Process p = pb.start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String err = new String(p.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
- assertTrue(p.waitFor(30, TimeUnit.SECONDS), "wrapper did not exit in time");
+ assertTrue(p.waitFor(60, TimeUnit.SECONDS), "wrapper did not exit in time");
Result r = new Result();
r.exit = p.exitValue();
r.stdout = out;
@@ -65,6 +85,68 @@ private Result run(String... args) throws Exception {
return r;
}
+ /**
+ * Creates a fake mvn.cmd on PATH that records its arguments instead of running JReleaser, so website-staging tests
+ * can prove the staging step ran (or did not run) without depending on jreleaser.yml existing yet.
+ */
+ private Map testModeEnvWithMvnStub(Path tmp, Path recordFile) throws IOException {
+ Path stubDir = tmp.resolve("stub-bin");
+ Files.createDirectories(stubDir);
+ Path mvnStub = stubDir.resolve("mvn.cmd");
+ Files.writeString(mvnStub, "@echo off\r\necho %* >> \"" + recordFile + "\"\r\nexit /b 0\r\n",
+ StandardCharsets.UTF_8);
+
+ Map env = new LinkedHashMap<>();
+ env.put("CAMEL_PACKAGE_TEST_MODE", "true");
+ env.put("CAMEL_PACKAGE_TEST_VERSION", TEST_VERSION);
+ env.put("PATH", stubDir + java.io.File.pathSeparator + System.getenv("PATH"));
+ return env;
+ }
+
+ private Path writeReleaseFixture(String suffix, String content) throws IOException {
+ Path target = MODULE_DIR.resolve("target");
+ Files.createDirectories(target);
+ Path file = target.resolve("camel-launcher-" + TEST_VERSION + suffix);
+ Files.writeString(file, content, StandardCharsets.UTF_8);
+ return file;
+ }
+
+ private Path websiteDir() {
+ return MODULE_DIR.resolve("target/jreleaser/website");
+ }
+
+ @AfterEach
+ void cleanupFixtures() throws IOException {
+ Files.deleteIfExists(MODULE_DIR.resolve("target/camel-launcher-" + TEST_VERSION + "-bin.tar.gz"));
+ Files.deleteIfExists(MODULE_DIR.resolve("target/camel-launcher-" + TEST_VERSION + "-bin.zip"));
+ deleteRecursively(websiteDir());
+ }
+
+ private void deleteRecursively(Path dir) throws IOException {
+ if (!Files.exists(dir)) {
+ return;
+ }
+ try (var stream = Files.walk(dir)) {
+ stream.sorted(Comparator.reverseOrder()).forEach(p -> {
+ try {
+ Files.delete(p);
+ } catch (IOException e) {
+ throw new java.io.UncheckedIOException(e);
+ }
+ });
+ }
+ }
+
+ private String sha256Hex(Path file) throws Exception {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] digest = md.digest(Files.readAllBytes(file));
+ StringBuilder sb = new StringBuilder(digest.length * 2);
+ for (byte b : digest) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
@Test
void stableSelectsAllFivePackagersWithSdkmanDefault() throws Exception {
Result r = run("prepare", "--channel", "stable", "--print-plan");
@@ -94,4 +176,89 @@ void rejectsUnsupportedLtsLine() throws Exception {
assertEquals(2, r.exit);
}
+
+ @Test
+ void stableStagesInstallersAndWritesWebsiteManifests(@TempDir Path tmp) throws Exception {
+ Path tar = writeReleaseFixture("-bin.tar.gz", "fixture-tar");
+ Path zip = writeReleaseFixture("-bin.zip", "fixture-zip");
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertEquals(0, r.exit, r.stderr);
+ Path installBat = websiteDir().resolve("install.sh");
+ Path installPs1 = websiteDir().resolve("install.ps1");
+ assertArrayEquals(Files.readAllBytes(MODULE_DIR.resolve("src/install/install.sh")),
+ Files.readAllBytes(installBat));
+ assertArrayEquals(Files.readAllBytes(MODULE_DIR.resolve("src/install/install.ps1")),
+ Files.readAllBytes(installPs1));
+
+ Path versionManifest = websiteDir().resolve("camel-cli/releases/" + TEST_VERSION + ".properties");
+ Path latestManifest = websiteDir().resolve("camel-cli/latest.properties");
+ assertTrue(Files.exists(versionManifest));
+ assertTrue(Files.exists(latestManifest));
+ String expected = "format=1\nversion=" + TEST_VERSION + "\ntar_sha256=" + sha256Hex(tar) + "\nzip_sha256="
+ + sha256Hex(zip) + "\n";
+ assertEquals(expected, Files.readString(versionManifest, StandardCharsets.UTF_8));
+ assertArrayEquals(Files.readAllBytes(versionManifest), Files.readAllBytes(latestManifest));
+
+ assertTrue(Files.exists(recordFile), "the JReleaser (stubbed mvn) step must be reached once staging succeeds");
+ String recorded = Files.readString(recordFile, StandardCharsets.UTF_8);
+ assertTrue(recorded.contains("jreleaser:config"), recorded);
+ }
+
+ @Test
+ void ltsStagesInstallersButDoesNotWriteLatest(@TempDir Path tmp) throws Exception {
+ writeReleaseFixture("-bin.tar.gz", "fixture-tar-lts");
+ writeReleaseFixture("-bin.zip", "fixture-zip-lts");
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "lts", "--lts-line", "4.18");
+
+ assertEquals(0, r.exit, r.stderr);
+ assertTrue(Files.exists(websiteDir().resolve("camel-cli/releases/" + TEST_VERSION + ".properties")));
+ assertFalse(Files.exists(websiteDir().resolve("camel-cli/latest.properties")),
+ "LTS prepare must not create or modify latest.properties");
+ assertTrue(Files.exists(recordFile));
+ }
+
+ @Test
+ void snapshotVersionFailsBeforeReachingJReleaser(@TempDir Path tmp) throws Exception {
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = new LinkedHashMap<>(testModeEnvWithMvnStub(tmp, recordFile));
+ env.put("CAMEL_PACKAGE_TEST_VERSION", "9.9.9-SNAPSHOT");
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertNotEquals(0, r.exit, "a snapshot version must be rejected");
+ assertFalse(Files.exists(recordFile), "JReleaser must never be invoked for a snapshot version");
+ assertFalse(Files.exists(websiteDir()));
+ }
+
+ @Test
+ void missingArtifactFailsBeforeReachingJReleaser(@TempDir Path tmp) throws Exception {
+ // Deliberately omit the release TAR/ZIP fixtures for TEST_VERSION.
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertNotEquals(0, r.exit, "missing release artifacts must fail before JReleaser runs");
+ assertFalse(Files.exists(recordFile));
+ assertFalse(Files.exists(websiteDir()));
+ }
+
+ @Test
+ void testVersionOverrideRequiresExplicitTestMode(@TempDir Path tmp) throws Exception {
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+ env.remove("CAMEL_PACKAGE_TEST_MODE");
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertEquals(2, r.exit, "CAMEL_PACKAGE_TEST_VERSION alone (without test mode) must be a fatal usage error");
+ assertFalse(Files.exists(recordFile));
+ }
}
diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanShTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanShTest.java
index abd62bd8b5b0a..46245a4d0aa5c 100644
--- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanShTest.java
+++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanShTest.java
@@ -16,19 +16,29 @@
*/
package org.apache.camel.dsl.jbang.launcher;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.security.MessageDigest;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -39,6 +49,10 @@
class PackagePlanShTest {
private static final Path SCRIPT = Paths.get("src/jreleaser/bin/camel-package.sh");
+ private static final Path MODULE_DIR = Paths.get("").toAbsolutePath();
+
+ /** Fixture release version used by the website-staging integration tests. Never a real release. */
+ private static final String TEST_VERSION = "9.9.9";
private static final class Result {
int exit;
@@ -47,16 +61,22 @@ private static final class Result {
}
private Result run(String... args) throws Exception {
+ return run(Map.of(), args);
+ }
+
+ private Result run(Map extraEnv, String... args) throws Exception {
List cmd = new ArrayList<>();
cmd.add("/bin/sh");
cmd.add(SCRIPT.toString());
for (String a : args) {
cmd.add(a);
}
- Process p = new ProcessBuilder(cmd).start();
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.environment().putAll(extraEnv);
+ Process p = pb.start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String err = new String(p.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
- assertTrue(p.waitFor(30, TimeUnit.SECONDS), "wrapper did not exit in time");
+ assertTrue(p.waitFor(60, TimeUnit.SECONDS), "wrapper did not exit in time");
Result r = new Result();
r.exit = p.exitValue();
r.stdout = out;
@@ -64,6 +84,69 @@ private Result run(String... args) throws Exception {
return r;
}
+ /**
+ * Creates a fake `mvn` on PATH that records its arguments instead of running JReleaser, so website-staging tests
+ * can prove the staging step ran (or did not run) without depending on jreleaser.yml existing yet.
+ */
+ private Map testModeEnvWithMvnStub(Path tmp, Path recordFile) throws IOException {
+ Path stubDir = tmp.resolve("stub-bin");
+ Files.createDirectories(stubDir);
+ Path mvnStub = stubDir.resolve("mvn");
+ Files.writeString(mvnStub, "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"" + recordFile + "\"\nexit 0\n",
+ StandardCharsets.UTF_8);
+ assertTrue(mvnStub.toFile().setExecutable(true));
+
+ Map env = new LinkedHashMap<>();
+ env.put("CAMEL_PACKAGE_TEST_MODE", "true");
+ env.put("CAMEL_PACKAGE_TEST_VERSION", TEST_VERSION);
+ env.put("PATH", stubDir + java.io.File.pathSeparator + System.getenv("PATH"));
+ return env;
+ }
+
+ private Path writeReleaseFixture(String suffix, String content) throws IOException {
+ Path target = MODULE_DIR.resolve("target");
+ Files.createDirectories(target);
+ Path file = target.resolve("camel-launcher-" + TEST_VERSION + suffix);
+ Files.writeString(file, content, StandardCharsets.UTF_8);
+ return file;
+ }
+
+ private Path websiteDir() {
+ return MODULE_DIR.resolve("target/jreleaser/website");
+ }
+
+ @AfterEach
+ void cleanupFixtures() throws IOException {
+ Files.deleteIfExists(MODULE_DIR.resolve("target/camel-launcher-" + TEST_VERSION + "-bin.tar.gz"));
+ Files.deleteIfExists(MODULE_DIR.resolve("target/camel-launcher-" + TEST_VERSION + "-bin.zip"));
+ deleteRecursively(websiteDir());
+ }
+
+ private void deleteRecursively(Path dir) throws IOException {
+ if (!Files.exists(dir)) {
+ return;
+ }
+ try (var stream = Files.walk(dir)) {
+ stream.sorted(Comparator.reverseOrder()).forEach(p -> {
+ try {
+ Files.delete(p);
+ } catch (IOException e) {
+ throw new java.io.UncheckedIOException(e);
+ }
+ });
+ }
+ }
+
+ private String sha256Hex(Path file) throws Exception {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] digest = md.digest(Files.readAllBytes(file));
+ StringBuilder sb = new StringBuilder(digest.length * 2);
+ for (byte b : digest) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
@Test
void stableSelectsAllFivePackagersWithSdkmanDefault() throws Exception {
Result r = run("prepare", "--channel", "stable", "--print-plan");
@@ -137,4 +220,90 @@ void rejectsUnknownSubcommand() throws Exception {
assertEquals(2, r.exit);
}
+
+ @Test
+ void stableStagesInstallersAndWritesWebsiteManifests(@TempDir Path tmp) throws Exception {
+ Path tar = writeReleaseFixture("-bin.tar.gz", "fixture-tar");
+ Path zip = writeReleaseFixture("-bin.zip", "fixture-zip");
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertEquals(0, r.exit, r.stderr);
+ Path installSh = websiteDir().resolve("install.sh");
+ Path installPs1 = websiteDir().resolve("install.ps1");
+ assertArrayEquals(Files.readAllBytes(MODULE_DIR.resolve("src/install/install.sh")),
+ Files.readAllBytes(installSh));
+ assertArrayEquals(Files.readAllBytes(MODULE_DIR.resolve("src/install/install.ps1")),
+ Files.readAllBytes(installPs1));
+
+ Path versionManifest = websiteDir().resolve("camel-cli/releases/" + TEST_VERSION + ".properties");
+ Path latestManifest = websiteDir().resolve("camel-cli/latest.properties");
+ assertTrue(Files.exists(versionManifest));
+ assertTrue(Files.exists(latestManifest));
+ String expected = "format=1\nversion=" + TEST_VERSION + "\ntar_sha256=" + sha256Hex(tar) + "\nzip_sha256="
+ + sha256Hex(zip) + "\n";
+ assertEquals(expected, Files.readString(versionManifest, StandardCharsets.UTF_8));
+ assertArrayEquals(Files.readAllBytes(versionManifest), Files.readAllBytes(latestManifest));
+
+ assertTrue(Files.exists(recordFile), "the JReleaser (stubbed mvn) step must be reached once staging succeeds");
+ String recorded = Files.readString(recordFile, StandardCharsets.UTF_8);
+ assertTrue(recorded.contains("jreleaser:config"), recorded);
+ }
+
+ @Test
+ void ltsStagesInstallersButDoesNotWriteLatest(@TempDir Path tmp) throws Exception {
+ writeReleaseFixture("-bin.tar.gz", "fixture-tar-lts");
+ writeReleaseFixture("-bin.zip", "fixture-zip-lts");
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "lts", "--lts-line", "4.18");
+
+ assertEquals(0, r.exit, r.stderr);
+ assertTrue(Files.exists(websiteDir().resolve("camel-cli/releases/" + TEST_VERSION + ".properties")));
+ assertFalse(Files.exists(websiteDir().resolve("camel-cli/latest.properties")),
+ "LTS prepare must not create or modify latest.properties");
+ assertTrue(Files.exists(recordFile));
+ }
+
+ @Test
+ void snapshotVersionFailsBeforeReachingJReleaser(@TempDir Path tmp) throws Exception {
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = new LinkedHashMap<>(testModeEnvWithMvnStub(tmp, recordFile));
+ env.put("CAMEL_PACKAGE_TEST_VERSION", "9.9.9-SNAPSHOT");
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertNotEquals(0, r.exit, "a snapshot version must be rejected");
+ assertFalse(Files.exists(recordFile), "JReleaser must never be invoked for a snapshot version");
+ assertFalse(Files.exists(websiteDir()));
+ }
+
+ @Test
+ void missingArtifactFailsBeforeReachingJReleaser(@TempDir Path tmp) throws Exception {
+ // Deliberately omit the release TAR/ZIP fixtures for TEST_VERSION.
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertNotEquals(0, r.exit, "missing release artifacts must fail before JReleaser runs");
+ assertFalse(Files.exists(recordFile));
+ assertFalse(Files.exists(websiteDir()));
+ }
+
+ @Test
+ void testVersionOverrideRequiresExplicitTestMode(@TempDir Path tmp) throws Exception {
+ Path recordFile = tmp.resolve("mvn-calls.txt");
+ Map env = testModeEnvWithMvnStub(tmp, recordFile);
+ env.remove("CAMEL_PACKAGE_TEST_MODE");
+
+ Result r = run(env, "prepare", "--channel", "stable");
+
+ assertEquals(2, r.exit, "CAMEL_PACKAGE_TEST_VERSION alone (without test mode) must be a fatal usage error");
+ assertTrue(r.stderr.toLowerCase().contains("test_mode"), r.stderr);
+ assertFalse(Files.exists(recordFile));
+ }
}
From 45b4320185f46334910885d1172ab37e5cdc9267 Mon Sep 17 00:00:00 2001
From: Adriano Machado <60320+ammachado@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:17:57 -0400
Subject: [PATCH 13/16] CAMEL-23703: camel-launcher - jreleaser.yml for five
packagers, verified against 1.25.0
Authors jreleaser.yml with one JAVA_BINARY distribution and the brew, sdkman,
winget, scoop, and chocolatey packagers, per the knowledge-cutoff rule this was
validated against live JReleaser 1.25.0 docs and, more importantly, against
actual jreleaser:config/jreleaser:prepare dry-runs using the pinned plugin JAR
rather than assumed from documentation alone. That empirical pass caught several
real defects:
- pom.xml's was wrongly nested inside a wrapper element
(a bug from the earlier plugin-pinning commit); it must be a direct
child.
- `type: BINARY` does not carry Java-distribution semantics; JAVA_BINARY does,
confirmed by the generated Homebrew formula correctly auto-adding
`depends_on "openjdk@17"`.
- `project.java` is deprecated in 1.25.0 in favor of `project.languages.java`.
- `project.vendor` and `distributions.camel-cli.tags` are required in practice
despite docs implying defaults; validation fails without them.
- Every packager defaults to a GitHub-release download URL. Camel publishes to
Maven Central, not GitHub release assets, so each packager now sets an explicit
`downloadUrl` pointing at the same repo1.maven.org coordinates Phase 3A's
installers and docs/getting-started.adoc already use.
- `{{ Env.X }}` templates resolve real OS environment variables
(System#getenv), not Maven -D system properties, so the wrappers now export
CAMEL_PKG_BREW_FORMULA instead of passing -Dcamel.pkg.brewFormula. Enum fields
(active, sdkman.command) are parsed before templates resolve and cannot use
`{{ Env.X }}` at all; a nested system-property override was tried for
sdkman.command and confirmed (via trace.log) not to take effect. SDKMAN has no
local `prepare` output - `command` only affects the live publish-time API call -
so per-channel MAJOR/MINOR selection is deferred to Phase 5.
- Per-channel packager selection (e.g. `lts` excluding `scoop`) uses the
jreleaser:prepare/package Mojo's own -Djreleaser.distributions/-Djreleaser.packagers
include filters (confirmed via `mvn help:describe` on the pinned plugin), not a
templated `active` field.
- A placeholder JRELEASER_GITHUB_TOKEN is exported only when unset, since
-Djreleaser.dry.run=true never performs a network call but config validation
still requires a non-blank token.
Also fixes a recurring camel-package.bat license-header duplication: the
license-maven-plugin's default .bat style doesn't recognize a header following
the required leading `@echo off` line and re-prepends a duplicate on every
`license:format` run. Excluded the file from that goal at the module level
(same pattern already used by components/camel-diagram/pom.xml) rather than
patching the symptom repeatedly.
Known gap: `--channel stable --lts-line X.Y` is specified to publish both an
unversioned and a versioned Homebrew formula from one release; this file
declares only one `brew` packager per distribution, so today it only produces
the formula selected by CAMEL_PKG_BREW_FORMULA. Deferred to a follow-up task.
Co-Authored-By: Claude Sonnet 5
---
dsl/camel-jbang/camel-launcher/jreleaser.yml | 100 ++++++++++++++++++
dsl/camel-jbang/camel-launcher/pom.xml | 25 ++++-
.../src/jreleaser/bin/camel-package.bat | 17 ++-
.../src/jreleaser/bin/camel-package.sh | 26 +++--
4 files changed, 156 insertions(+), 12 deletions(-)
create mode 100644 dsl/camel-jbang/camel-launcher/jreleaser.yml
diff --git a/dsl/camel-jbang/camel-launcher/jreleaser.yml b/dsl/camel-jbang/camel-launcher/jreleaser.yml
new file mode 100644
index 0000000000000..128fe8d758f8c
--- /dev/null
+++ b/dsl/camel-jbang/camel-launcher/jreleaser.yml
@@ -0,0 +1,100 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# NOTE: field names and packager semantics verified against the JReleaser 1.25.0
+# reference (ctx7 docs, cross-checked with `mvn help:describe` and actual
+# `jreleaser:prepare` dry-run output against the pinned plugin JAR) before
+# authoring this file. Key findings that shaped this configuration:
+# - `type: JAVA_BINARY` (not BINARY): Homebrew's automatic `openjdk` dependency is
+# documented as applying to "applicable Java distributions"; confirmed the
+# generated formula includes `depends_on "openjdk@17"`.
+# - SDKMAN has no boolean "default" flag; `command: MAJOR` publishes and sets the
+# new version as the candidate default, `command: MINOR` publishes without
+# moving the default. SDKMAN produces no local `prepare` output (API-only), so
+# channel-specific MAJOR/MINOR selection only matters at Phase 5 publish time.
+# - `{{ Env.X }}` reads real OS environment variables (java.lang.System#getenv),
+# not Maven -D system properties, so the wrappers export CAMEL_PKG_* before
+# invoking JReleaser. Enum-typed fields (active, sdkman.command) are parsed
+# before Mustache templates resolve, so `{{ Env.X }}` cannot be used on them.
+# - Artifact paths are relative to the repo root: empirically (mvn jreleaser:config),
+# the JReleaser Maven plugin's basedir defaults to the multi-module reactor root,
+# not this file's directory, unlike the standalone JReleaser CLI.
+# - Per-channel packager selection (e.g. excluding `scoop` on the `lts` channel)
+# is done via the jreleaser:prepare/jreleaser:package Mojo's own
+# `-Djreleaser.packagers` include filter, not via a templated `active` field.
+# - Every packager overrides `downloadUrl` to the Maven Central coordinates
+# (matching Phase 3A's installers and docs/getting-started.adoc); JReleaser's
+# default download URL points at a GitHub release asset, which this project
+# does not publish artifacts to.
+# - `project.vendor` and `distributions.camel-cli.tags` are required fields in
+# practice (validation fails without them) despite docs implying defaults.
+# This file renders no package content itself; per-packager overrides live under
+# src/jreleaser/distributions/camel-cli//.
+#
+# KNOWN GAP: `--channel stable --lts-line X.Y` is specified to also publish a
+# versioned Homebrew formula (`camel@X.Y`) alongside the unversioned `camel`
+# formula. This file declares only one `brew` packager per distribution, so today
+# it only ever produces the single formula selected by CAMEL_PKG_BREW_FORMULA.
+# Producing both formulae from one release is deferred to a follow-up task.
+
+project:
+ name: camel-cli
+ description: Apache Camel CLI
+ links:
+ homepage: https://camel.apache.org/
+ authors:
+ - The Apache Camel Team
+ license: Apache-2.0
+ vendor: The Apache Software Foundation
+ languages:
+ java:
+ groupId: org.apache.camel
+ version: 17
+
+distributions:
+ camel-cli:
+ type: JAVA_BINARY
+ tags:
+ - cli
+ executable:
+ name: camel
+ artifacts:
+ - path: "dsl/camel-jbang/camel-launcher/target/camel-launcher-{{projectVersion}}-bin.zip"
+ brew:
+ active: RELEASE
+ formulaName: "{{ Env.CAMEL_PKG_BREW_FORMULA }}"
+ downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
+ sdkman:
+ active: RELEASE
+ candidate: camel
+ downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
+ # command (MAJOR/MINOR) defaults to MAJOR here (correct for `stable`); see the
+ # NOTE above on why per-channel selection is deferred to Phase 5 publish.
+ winget:
+ active: RELEASE
+ downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
+ package:
+ identifier: Apache.CamelCLI
+ name: Camel CLI
+ scoop:
+ active: RELEASE
+ downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
+ chocolatey:
+ active: RELEASE
+ packageName: camel
+ remoteBuild: true
+ downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
diff --git a/dsl/camel-jbang/camel-launcher/pom.xml b/dsl/camel-jbang/camel-launcher/pom.xml
index b5f35e69bd572..e1470e08698d6 100644
--- a/dsl/camel-jbang/camel-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-launcher/pom.xml
@@ -342,9 +342,28 @@
${jreleaser-plugin-version}
false
-
- ${project.basedir}/jreleaser.yml
-
+ ${project.basedir}/jreleaser.yml
+ true
+
+
+
+
+ com.mycila
+ license-maven-plugin
+
+
+
+
+ src/jreleaser/bin/camel-package.bat
+
+
+
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
index 142e92ff00158..44fd77f15c274 100644
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
@@ -176,8 +176,23 @@ if errorlevel 1 (
exit /b 1
)
+@REM jreleaser.yml reads the Homebrew formula name via `{{ Env.CAMEL_PKG_BREW_FORMULA }}`.
+@REM JReleaser's `Env.` template prefix resolves real OS environment variables, not
+@REM Maven -D system properties, so this must be a real env var. Verified empirically
+@REM with `jreleaser:prepare`.
+set "CAMEL_PKG_BREW_FORMULA=!BREW_FORMULA!"
+
+@REM `-Djreleaser.dry.run=true` below never performs a network call, so a placeholder
+@REM satisfies JReleaser's config validation (it requires a non-blank release token)
+@REM without requiring a real credential. Never overrides a token the caller already set.
+if "%JRELEASER_GITHUB_TOKEN%"=="" set "JRELEASER_GITHUB_TOKEN=dry-run-placeholder"
+
+@REM `-Djreleaser.distributions` / `-Djreleaser.packagers` are the JReleaser Maven
+@REM plugin's own include filters (confirmed via `mvn help:describe` on the pinned
+@REM 1.25.0 plugin), used to select which packagers run for this channel; e.g. `lts`
+@REM excludes `scoop` by omitting it from !PACKAGERS!.
echo Preparing packages for channel '%CHANNEL%' ^(packagers: !PACKAGERS!^)...
-call mvn -B -ntp -pl "%MODULE_DIR%" -Dcamel.pkg.channel=%CHANNEL% -Dcamel.pkg.ltsLine=%LTS_LINE% -Dcamel.pkg.packagers=!PACKAGERS! -Dcamel.pkg.brewFormula=!BREW_FORMULA! -Dcamel.pkg.brewLtsFormula=!BREW_LTS_FORMULA! -Dcamel.pkg.sdkmanDefault=!SDKMAN_DEFAULT! jreleaser:config jreleaser:prepare jreleaser:package -Djreleaser.dry.run=true
+call mvn -B -ntp -pl "%MODULE_DIR%" -Djreleaser.distributions=camel-cli -Djreleaser.packagers=!PACKAGERS! jreleaser:config jreleaser:prepare jreleaser:package -Djreleaser.dry.run=true
exit /b %ERRORLEVEL%
:usage
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
index ee0afb5bdbaae..8fc4f147ee379 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
@@ -182,15 +182,25 @@ if ! java "$MODULE_DIR/src/jreleaser/java/WebsiteManifestGenerator.java" \
exit 1
fi
-# Map the plan to JReleaser system properties. Actual property names are validated
-# against the JReleaser 1.25.0 reference during template work (Tasks 7-9).
+# jreleaser.yml reads the Homebrew formula name via `{{ Env.CAMEL_PKG_BREW_FORMULA }}`.
+# JReleaser's `Env.` template prefix resolves real OS environment variables
+# (java.lang.System#getenv), not Maven -D system properties, so this must be
+# exported rather than passed as -D. Verified empirically with `jreleaser:prepare`.
+export CAMEL_PKG_BREW_FORMULA="$BREW_FORMULA"
+
+# `-Djreleaser.dry.run=true` below never performs a network call, so a placeholder
+# satisfies JReleaser's config validation (it requires a non-blank release token)
+# without requiring a real credential. Never overrides a token the caller already set.
+: "${JRELEASER_GITHUB_TOKEN:=dry-run-placeholder}"
+export JRELEASER_GITHUB_TOKEN
+
+# `-Djreleaser.distributions` / `-Djreleaser.packagers` are the JReleaser Maven
+# plugin's own include filters (confirmed via `mvn help:describe` on the pinned
+# 1.25.0 plugin), used to select which packagers run for this channel; e.g. `lts`
+# excludes `scoop` by omitting it from $PACKAGERS.
echo "Preparing packages for channel '$CHANNEL' (packagers: $PACKAGERS)..."
mvn -B -ntp -pl "$MODULE_DIR" \
- -Dcamel.pkg.channel="$CHANNEL" \
- -Dcamel.pkg.ltsLine="$LTS_LINE" \
- -Dcamel.pkg.packagers="$PACKAGERS" \
- -Dcamel.pkg.brewFormula="$BREW_FORMULA" \
- -Dcamel.pkg.brewLtsFormula="$BREW_LTS_FORMULA" \
- -Dcamel.pkg.sdkmanDefault="$SDKMAN_DEFAULT" \
+ -Djreleaser.distributions=camel-cli \
+ -Djreleaser.packagers="$PACKAGERS" \
jreleaser:config jreleaser:prepare jreleaser:package \
-Djreleaser.dry.run=true
From 7184aa99a5203c8e057f204eb1a279e1e92ea663 Mon Sep 17 00:00:00 2001
From: Adriano Machado <60320+ammachado@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:39:00 -0400
Subject: [PATCH 14/16] CAMEL-23703: camel-launcher - Homebrew + SDKMAN
template overrides (caveats/notes)
Authored a custom formula.rb.tpl (ground truth extracted via the pinned
plugin's jreleaser:template-generate goal, not guessed) that:
- depends_on the unversioned Homebrew "openjdk" (Camel only needs 17+, so the
always-current formula never goes stale, unlike depends_on "openjdk@17").
- Installs a CAMEL_FALLBACK_JAVA-exporting wrapper via write_env_script instead
of a bare symlink, so `camel` works even with no other Java on PATH.
- Emits the required caveat text, plus a keg_only :versioned_formula caveat for
versioned formulae, gated by a new brewVersionedFormula extraProperty (empty
string is Mustache-falsy) computed by both wrapper scripts.
Empirical jreleaser:prepare dry-runs against the pinned plugin surfaced two
real bugs beyond the plan's stated scope, both fixed here:
- distributions.camel-cli.executable had no unixExtension, so
distributionExecutableUnix defaulted to "camel" instead of the actual
on-disk "camel.sh" (see src/main/assembly/bin.xml), producing a broken
symlink in every packager that installs from libexec (Homebrew, and by
the same mechanism Scoop/Chocolatey in Task 8).
- The brew packager's templateDirectory default resolves against the
JReleaser Maven plugin's basedir (the reactor root, confirmed in Task 6),
not this module's directory, so formula.rb.tpl was silently ignored until
templateDirectory was set explicitly to the module-relative path.
Testing a versioned formula name (camel@4.20) also surfaced that JReleaser
does not apply Homebrew's "AT" class/filename convention to formulaName,
producing an invalid Ruby class and wrong output filename; documented as an
extension of the existing "KNOWN GAP" note rather than fixed here, since a
correct fix needs formulaName pre-conversion, out of Task 7's scope.
SDKMAN has no local template extension point in 1.25.0 (confirmed: template-
generate writes no files for packagerName=sdkman); it only exposes
releaseNotesUrl, with actual candidate publication via its Vendor API at
Phase 5 publish time. The required note text is stored as a plain repo asset
for Phase 5 to consume, documented as not yet wired into JReleaser.
Also removes the incidental cask.rb.tpl/README.md.tpl scaffold files (this
distribution has no cask, and JReleaser falls back to its own embedded
default README.md.tpl per-file when one isn't provided).
Co-Authored-By: Claude Sonnet 5
---
dsl/camel-jbang/camel-launcher/jreleaser.yml | 23 ++++++
dsl/camel-jbang/camel-launcher/pom.xml | 9 +++
.../src/jreleaser/bin/camel-package.bat | 5 ++
.../src/jreleaser/bin/camel-package.sh | 8 ++
.../camel-cli/brew/formula.rb.tpl | 81 +++++++++++++++++++
.../camel-cli/sdkman/release-notes.md | 31 +++++++
6 files changed, 157 insertions(+)
create mode 100644 dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/brew/formula.rb.tpl
create mode 100644 dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/sdkman/release-notes.md
diff --git a/dsl/camel-jbang/camel-launcher/jreleaser.yml b/dsl/camel-jbang/camel-launcher/jreleaser.yml
index 128fe8d758f8c..f68a12b0ae8e0 100644
--- a/dsl/camel-jbang/camel-launcher/jreleaser.yml
+++ b/dsl/camel-jbang/camel-launcher/jreleaser.yml
@@ -50,6 +50,12 @@
# formula. This file declares only one `brew` packager per distribution, so today
# it only ever produces the single formula selected by CAMEL_PKG_BREW_FORMULA.
# Producing both formulae from one release is deferred to a follow-up task.
+# Empirically confirmed (jreleaser:prepare with CAMEL_PKG_BREW_FORMULA=camel@4.20):
+# JReleaser does not apply Homebrew's "AT" naming convention to `formulaName`, so a
+# literal "camel@4.20" renders an invalid Ruby class (`class Camel@4.20 < Formula`)
+# and a wrong output filename (`20.rb` instead of `camel@4.20.rb`). Producing a
+# correct versioned formula needs formulaName pre-converted to Homebrew's
+# convention (e.g. "CamelAT420") as part of the same follow-up task above.
project:
name: camel-cli
@@ -72,12 +78,29 @@ distributions:
- cli
executable:
name: camel
+ # The zip/tar bin/ directory ships `camel.sh` (Unix) and `camel.bat`/`camel.exe`
+ # (Windows) per src/main/assembly/bin.xml; without this, `distributionExecutableUnix`
+ # defaults to the bare distribution name ("camel"), which does not exist on disk and
+ # produces a broken symlink in the Homebrew/Scoop/Chocolatey templates. Windows already
+ # defaults correctly to `bat`.
+ unixExtension: sh
artifacts:
- path: "dsl/camel-jbang/camel-launcher/target/camel-launcher-{{projectVersion}}-bin.zip"
brew:
active: RELEASE
formulaName: "{{ Env.CAMEL_PKG_BREW_FORMULA }}"
downloadUrl: "https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/{{projectVersion}}/{{artifactFile}}"
+ # templateDirectory defaults to "src/jreleaser/distributions/#{distribution.name}/brew"
+ # resolved against the JReleaser basedir, which is the multi-module reactor root (see
+ # NOTE above), not this module's directory. Empirically confirmed (jreleaser:prepare
+ # trace.log): without this override the module's formula.rb.tpl is silently ignored
+ # and JReleaser falls back to its embedded default template.
+ templateDirectory: "dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/brew"
+ extraProperties:
+ # Non-empty only for versioned formulae (e.g. "camel@4.20"); used by
+ # formula.rb.tpl as a Mustache truthiness check (empty string == falsy)
+ # to add `keg_only :versioned_formula` and its PATH caveat.
+ versionedFormula: "{{ Env.CAMEL_PKG_BREW_VERSIONED }}"
sdkman:
active: RELEASE
candidate: camel
diff --git a/dsl/camel-jbang/camel-launcher/pom.xml b/dsl/camel-jbang/camel-launcher/pom.xml
index e1470e08698d6..b3421ad0af119 100644
--- a/dsl/camel-jbang/camel-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-launcher/pom.xml
@@ -352,6 +352,14 @@
so `license:format` keeps prepending a duplicate header on every run. The file
already carries a correct, manually-verified header, so it is excluded here
rather than reordering `@echo off` out of the required first line.
+
+ formula.rb.tpl has no recognized comment-style mapping for the compound
+ ".rb.tpl" extension (license:format only inspects the final extension, ".tpl",
+ which is unmapped), so it fails outright rather than being reformatted. The
+ file is a Ruby (Homebrew Formula) template and already carries a manually
+ verified Ruby-style (#) header, so it is excluded here instead of adding a
+ repo-wide ".tpl" mapping that would assume a single comment style for every
+ future template of any language.
-->
com.mycila
@@ -361,6 +369,7 @@
src/jreleaser/bin/camel-package.bat
+ src/jreleaser/distributions/camel-cli/brew/formula.rb.tpl
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
index 44fd77f15c274..c22729e9689f9 100644
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.bat
@@ -182,6 +182,11 @@ if errorlevel 1 (
@REM with `jreleaser:prepare`.
set "CAMEL_PKG_BREW_FORMULA=!BREW_FORMULA!"
+@REM Non-empty only for a versioned formula (e.g. "camel@4.20"); formula.rb.tpl uses
+@REM this to add `keg_only :versioned_formula` and its PATH caveat.
+set "CAMEL_PKG_BREW_VERSIONED="
+echo !BREW_FORMULA! | findstr /C:"@" >nul && set "CAMEL_PKG_BREW_VERSIONED=!BREW_FORMULA!"
+
@REM `-Djreleaser.dry.run=true` below never performs a network call, so a placeholder
@REM satisfies JReleaser's config validation (it requires a non-blank release token)
@REM without requiring a real credential. Never overrides a token the caller already set.
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
index 8fc4f147ee379..82287b77e2b97 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
@@ -188,6 +188,14 @@ fi
# exported rather than passed as -D. Verified empirically with `jreleaser:prepare`.
export CAMEL_PKG_BREW_FORMULA="$BREW_FORMULA"
+# Non-empty only for a versioned formula (e.g. "camel@4.20"); formula.rb.tpl uses
+# this to add `keg_only :versioned_formula` and its PATH caveat.
+case "$BREW_FORMULA" in
+ *@*) CAMEL_PKG_BREW_VERSIONED="$BREW_FORMULA" ;;
+ *) CAMEL_PKG_BREW_VERSIONED="" ;;
+esac
+export CAMEL_PKG_BREW_VERSIONED
+
# `-Djreleaser.dry.run=true` below never performs a network call, so a placeholder
# satisfies JReleaser's config validation (it requires a non-blank release token)
# without requiring a real credential. Never overrides a token the caller already set.
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/brew/formula.rb.tpl b/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/brew/formula.rb.tpl
new file mode 100644
index 0000000000000..fa5ff11f41198
--- /dev/null
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/brew/formula.rb.tpl
@@ -0,0 +1,81 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# {{jreleaserCreationStamp}}
+{{#brewRequireRelative}}
+require_relative "{{.}}"
+{{/brewRequireRelative}}
+
+class {{brewFormulaName}} < Formula
+ desc "{{projectDescription}}"
+ homepage "{{projectLinkHomepage}}"
+ url "{{distributionUrl}}"{{#brewDownloadStrategy}}, :using => {{.}}{{/brewDownloadStrategy}}
+ version "{{projectVersion}}"
+ sha256 "{{distributionChecksumSha256}}"
+ license "{{projectLicense}}"
+
+ {{#brewVersionedFormula}}
+ keg_only :versioned_formula
+ {{/brewVersionedFormula}}
+
+ {{#brewHasLivecheck}}
+ livecheck do
+ {{#brewLivecheck}}
+ {{.}}
+ {{/brewLivecheck}}
+ end
+ {{/brewHasLivecheck}}
+
+ # Unversioned (not "openjdk@17"): the launcher only requires Java 17+, and the
+ # plain formula always tracks a current release, so it never goes stale/EOL the
+ # way a version-pinned dependency would.
+ depends_on "openjdk"
+
+ def install
+ libexec.install Dir["*"]
+ # write_env_script wraps the upstream camel.sh with CAMEL_FALLBACK_JAVA set, so
+ # `camel` works out of the box even if no other Java is on PATH/JAVA_HOME.
+ (bin/"{{distributionExecutableName}}").write_env_script libexec/"bin/camel.sh",
+ CAMEL_FALLBACK_JAVA: "#{Formula["openjdk"].opt_bin}/java"
+ end
+
+ def caveats
+ s = <<~EOS
+ Apache Camel CLI installs its own Homebrew OpenJDK dependency even if a
+ compatible Java 17+ is already present. The launcher selects a Java runtime in
+ this order: JAVACMD, JAVA_HOME/bin/java, the first java on PATH, then
+ CAMEL_FALLBACK_JAVA (which this formula points at the Homebrew OpenJDK).
+
+ Run "camel version" to verify the install.
+ EOS
+ {{#brewVersionedFormula}}
+ s += <<~EOS
+
+ "#{name}" is keg-only: it was not symlinked into #{HOMEBREW_PREFIX} because
+ another version of this formula may also be installed. To use this version's
+ "camel" first in your PATH, run:
+ echo 'export PATH="#{opt_bin}:$PATH"' >> ~/.zshrc
+ EOS
+ {{/brewVersionedFormula}}
+ s
+ end
+
+ test do
+ output = shell_output("#{bin}/{{distributionExecutableName}} --version")
+ assert_match "{{projectVersion}}", output
+ end
+end
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/sdkman/release-notes.md b/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/sdkman/release-notes.md
new file mode 100644
index 0000000000000..b712776ef06b4
--- /dev/null
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/distributions/camel-cli/sdkman/release-notes.md
@@ -0,0 +1,31 @@
+
+
+
+
+Apache Camel CLI requires Java 17 or newer. If you do not already have a
+compatible Java, "sdk install java" provides one and SDKMAN will export
+JAVA_HOME for it; any other Java 17+ runtime works equally well. The bin/camel
+launcher discovers Java via JAVACMD, JAVA_HOME, PATH, then CAMEL_FALLBACK_JAVA,
+and prints a diagnostic listing these sources if none qualify.
From d7764615ed31225aee30892560e8cd5d4abdd99a Mon Sep 17 00:00:00 2001
From: Adriano Machado <60320+ammachado@users.noreply.github.com>
Date: Mon, 13 Jul 2026 10:27:02 -0400
Subject: [PATCH 15/16] CAMEL-23703: camel-launcher - fix packaging/installer
script bugs from review
Co-Authored-By: Claude Opus 4.8
---
dsl/camel-jbang/camel-launcher/src/install/install.sh | 3 ++-
.../camel-launcher/src/jreleaser/bin/camel-package.sh | 4 ++--
.../src/jreleaser/java/WebsiteManifestGenerator.java | 9 ++++++---
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/dsl/camel-jbang/camel-launcher/src/install/install.sh b/dsl/camel-jbang/camel-launcher/src/install/install.sh
index 5afadaf33bbbe..969415dca44be 100755
--- a/dsl/camel-jbang/camel-launcher/src/install/install.sh
+++ b/dsl/camel-jbang/camel-launcher/src/install/install.sh
@@ -152,7 +152,8 @@ validate_tar() {
verbose_listing="$staging_dir/listing-verbose.txt"
tar -tvzf "$archive" > "$verbose_listing" 2>/dev/null || fail "archive is not a valid tar.gz"
- grep -F -- ' -> ' "$verbose_listing" >/dev/null 2>&1 \
+ # tar -tv renders symlinks as 'name -> target' and hard links as 'name link to target'.
+ grep -E -- ' -> | link to ' "$verbose_listing" >/dev/null 2>&1 \
&& fail "archive contains a symbolic or hard link entry, which is not allowed"
roots=""
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
index 82287b77e2b97..e577d980910b9 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-package.sh
@@ -123,7 +123,7 @@ if [ -n "${CAMEL_PACKAGE_TEST_VERSION:-}" ]; then
fi
PROJECT_VERSION="$CAMEL_PACKAGE_TEST_VERSION"
else
- PROJECT_VERSION=`mvn -q -B -ntp -pl "$MODULE_DIR" org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \
+ PROJECT_VERSION=`mvn -q -B -ntp -f "$MODULE_DIR/pom.xml" org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \
-Dexpression=project.version -DforceStdout`
fi
@@ -207,7 +207,7 @@ export JRELEASER_GITHUB_TOKEN
# 1.25.0 plugin), used to select which packagers run for this channel; e.g. `lts`
# excludes `scoop` by omitting it from $PACKAGERS.
echo "Preparing packages for channel '$CHANNEL' (packagers: $PACKAGERS)..."
-mvn -B -ntp -pl "$MODULE_DIR" \
+mvn -B -ntp -f "$MODULE_DIR/pom.xml" \
-Djreleaser.distributions=camel-cli \
-Djreleaser.packagers="$PACKAGERS" \
jreleaser:config jreleaser:prepare jreleaser:package \
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
index fc0991244718b..1023f2bdcce34 100644
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
@@ -25,10 +25,13 @@
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
@@ -46,7 +49,7 @@ public class WebsiteManifestGenerator {
private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$");
private static final Set REQUIRED_OPTIONS
- = new LinkedHashSet<>(java.util.List.of("--version", "--tar", "--zip", "--output", "--latest"));
+ = new LinkedHashSet<>(List.of("--version", "--tar", "--zip", "--output", "--latest"));
private static final String MANIFEST_FORMAT = "1";
public static void main(String[] args) {
@@ -142,7 +145,7 @@ private static byte[] renderManifest(String version, String tarSha256, String zi
private static void writeVersionManifest(Path versionFile, byte[] manifest, String version) throws IOException {
if (Files.exists(versionFile)) {
byte[] existing = Files.readAllBytes(versionFile);
- if (java.util.Arrays.equals(existing, manifest)) {
+ if (Arrays.equals(existing, manifest)) {
return;
}
throw new ConflictException("version manifest for " + version + " already exists with different content"
@@ -214,7 +217,7 @@ private static int compareSemver(String a, String b) {
}
private static int[] semverParts(String version) {
- java.util.regex.Matcher m = VERSION_PATTERN.matcher(version);
+ Matcher m = VERSION_PATTERN.matcher(version);
if (!m.matches()) {
throw new ConflictException("invalid semantic version '" + version + "'.");
}
From 73d0694913d9622a3faa2fc262fd8a506bb3adea Mon Sep 17 00:00:00 2001
From: Adriano Machado <60320+ammachado@users.noreply.github.com>
Date: Tue, 14 Jul 2026 09:57:44 -0400
Subject: [PATCH 16/16] CAMEL-23703: camel-launcher - use imported Arrays in
WebsiteManifestGenerator
Replace the lone java.util.Arrays.equals FQCN in writeLatestManifest with the
already-imported Arrays.equals, matching writeVersionManifest and the project's
no-FQCN convention (the build's OpenRewrite step would otherwise rewrite it and
CI would fail on the uncommitted diff).
Co-Authored-By: Claude Opus 4.8
---
.../src/jreleaser/java/WebsiteManifestGenerator.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
index 1023f2bdcce34..fa9ff5b7213f8 100644
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/java/WebsiteManifestGenerator.java
@@ -161,7 +161,7 @@ private static void writeLatestManifest(Path latestFile, byte[] manifest, String
}
byte[] existing = Files.readAllBytes(latestFile);
- if (java.util.Arrays.equals(existing, manifest)) {
+ if (Arrays.equals(existing, manifest)) {
return;
}