This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new bfba9853dba1 CAMEL-23703: Add offline package-native validation CI for
camel-launcher
bfba9853dba1 is described below
commit bfba9853dba1d03a0c3c67a41dbcadb40b54ae62
Author: Adriano Machado <[email protected]>
AuthorDate: Sat Jul 25 16:29:36 2026 -0400
CAMEL-23703: Add offline package-native validation CI for camel-launcher
Adds CI workflow to validate the Camel CLI launcher distribution the way
real package managers consume it: POSIX validators for Homebrew and SDKMAN
on macOS/Linux, installer unit tests on Windows. Includes install.sh/ps1
hardening (manifest license-header tolerance, wget/tar error surfacing,
PowerShell 7 compatibility), Homebrew formula audit fixes, and shared TLS
cert optimization for test fixtures. Documentation updated with installer
guide and upgrade-guide entries.
Closes #25030
Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.github/workflows/package-native-validation.yml | 189 ++++++++
docs/user-manual/modules/ROOT/nav.adoc | 1 +
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 9 +
.../ROOT/pages/camel-jbang-launcher-install.adoc | 51 ++
dsl/camel-jbang/camel-launcher/jreleaser.yml | 6 +-
dsl/camel-jbang/camel-launcher/pom.xml | 6 +
.../camel-launcher/src/install/install.ps1 | 92 +++-
.../src/jreleaser/bin/camel-package.sh | 8 +-
.../src/jreleaser/bin/camel-validate.sh | 296 ++++++++----
.../src/jreleaser/bin/lib/assert-camel-cli.sh | 55 +--
.../launcher/PackageNativeValidationTest.java | 525 +++++++++++++++++++++
.../dsl/jbang/launcher/WebsiteInstallTest.java | 25 +-
.../jbang/launcher/WebsiteInstallerFixture.java | 70 ++-
13 files changed, 1175 insertions(+), 158 deletions(-)
diff --git a/.github/workflows/package-native-validation.yml
b/.github/workflows/package-native-validation.yml
new file mode 100644
index 000000000000..c9a42e2295e2
--- /dev/null
+++ b/.github/workflows/package-native-validation.yml
@@ -0,0 +1,189 @@
+#
+# 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.
+#
+
+name: Package-native validation
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'dsl/camel-jbang/camel-launcher/**'
+ - 'tooling/camel-exe/**'
+ - '.github/workflows/package-native-validation.yml'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'dsl/camel-jbang/camel-launcher/**'
+ - 'tooling/camel-exe/**'
+ - '.github/workflows/package-native-validation.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ # ── macOS / Linux (POSIX validators) ──────────────────────────────
+ posix-validator:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [macos-latest, ubuntu-latest]
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #
v7.0.1
+ with:
+ persist-credentials: false
+
+ # ubuntu-latest ships Homebrew (Linuxbrew) preinstalled but not on PATH
(confirmed
+ # via actions/runner-images' Ubuntu readme: "Homebrew is preinstalled on
image but
+ # not added to PATH", installed under /home/linuxbrew). Without this,
`command -v
+ # brew` in camel-validate.sh's host-gate fails on Linux and the Homebrew
validator
+ # silently SKIPs there, leaving macos-latest as the only leg that
actually exercises
+ # it. macos-latest already has Homebrew on PATH by default, so this step
is a no-op
+ # there.
+ - name: Add Linuxbrew to PATH (preinstalled but not on PATH on
ubuntu-latest)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ {
+ echo "/home/linuxbrew/.linuxbrew/bin"
+ echo "/home/linuxbrew/.linuxbrew/sbin"
+ } >> "$GITHUB_PATH"
+ {
+ echo "HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew"
+ echo "HOMEBREW_CELLAR=/home/linuxbrew/.linuxbrew/Cellar"
+ echo "HOMEBREW_REPOSITORY=/home/linuxbrew/.linuxbrew/Homebrew"
+ } >> "$GITHUB_ENV"
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 #
v5.6.0
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ cache: 'maven'
+
+ # camel-repackager-maven-plugin builds the launcher's self-executing
JAR. It is not in the
+ # -pl list below, so without this step Maven resolves it from the Apache
snapshot repository.
+ # This workflow only triggers on dsl/camel-jbang/camel-launcher and
tooling/camel-exe changes
+ # (see the `paths:` filters above), so the plugin's own sources are
never what's under test here.
+ - name: Install camel-repackager-maven-plugin (build-time plugin,
unresolvable from this reactor pass)
+ shell: bash
+ run: |
+ mvn -B -P apache-snapshots -ntp \
+ -pl tooling/maven/camel-repackager-maven-plugin install -DskipTests
+
+ - name: Build camel-launcher distribution
+ shell: bash
+ # No -am: build only the listed modules. camel-launcher's other
in-repo dependencies
+ # (camel-jbang-core, its jbang plugins, camel-core, ...) resolve as
the current version's
+ # published snapshot instead of being rebuilt from source, which is
what previously turned
+ # this into a ~626-module reactor build. -P apache-snapshots is
required for that resolution.
+ run: |
+ mvn -B -P apache-snapshots -ntp \
+ -pl buildingtools,dsl/camel-jbang/camel-launcher install
-DskipTests
+
+ - name: Stage a synthetic release version for offline validation
+ shell: bash
+ run: |
+ cd dsl/camel-jbang/camel-launcher
+ REAL_VERSION=$(mvn -q -B -ntp
org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \
+ -Dexpression=project.version -DforceStdout)
+ TEST_VERSION="${REAL_VERSION%-SNAPSHOT}"
+ echo "TEST_VERSION=$TEST_VERSION" >> "$GITHUB_ENV"
+ cp -p "target/camel-launcher-$REAL_VERSION-bin.tar.gz"
"target/camel-launcher-$TEST_VERSION-bin.tar.gz"
+ cp -p "target/camel-launcher-$REAL_VERSION-bin.zip"
"target/camel-launcher-$TEST_VERSION-bin.zip"
+ # camel-package.sh prepare requires a WinGet release ZIP
(camel-launcher-<v>-winget-bin.zip), but
+ # that artifact is only built by the -Dcamel.exe.build=true profile
(native camel-exe), which this
+ # validation build does not run. The POSIX validators never consume
the WinGet payload, so stage
+ # the already-built bin.zip under the WinGet name as an offline
stand-in. Two names are needed:
+ # camel-package.sh's own existence check and cmp look for the
stripped TEST_VERSION name (and
+ # CAMEL_PACKAGE_TEST_WINGET_REMOTE below points at the same bytes so
the cmp passes without a
+ # network fetch to archive.apache.org), while JReleaser resolves the
artifact from the real
+ # -SNAPSHOT POM version, so it needs the REAL_VERSION name too.
+ cp -p "target/camel-launcher-$REAL_VERSION-bin.zip"
"target/camel-launcher-$TEST_VERSION-winget-bin.zip"
+ cp -p "target/camel-launcher-$REAL_VERSION-bin.zip"
"target/camel-launcher-$REAL_VERSION-winget-bin.zip"
+
+ - name: Prepare packages (stable, offline, synthetic release version)
+ shell: bash
+ run: |
+ cd dsl/camel-jbang/camel-launcher
+ CAMEL_PACKAGE_TEST_MODE=true
CAMEL_PACKAGE_TEST_VERSION="$TEST_VERSION" \
+
CAMEL_PACKAGE_TEST_WINGET_REMOTE="target/camel-launcher-$TEST_VERSION-winget-bin.zip"
\
+ bash src/jreleaser/bin/camel-package.sh prepare --channel stable
+
+ - name: Validate local archive + Homebrew + SDKMAN (host-gated)
+ shell: bash
+ run: |
+ cd dsl/camel-jbang/camel-launcher
+ CAMEL_PACKAGE_TEST_MODE=true
CAMEL_PACKAGE_TEST_VERSION="$TEST_VERSION" \
+ bash src/jreleaser/bin/camel-validate.sh all
+
+ - name: Run POSIX validator unit tests
+ shell: bash
+ # WebsiteInstallTest's PosixShell suite only gets targeted CI coverage
today as a side
+ # effect of the generic incremental-build check noticing a direct
change under
+ # dsl/camel-jbang/camel-launcher; a PR that only touches an upstream
dependency wouldn't
+ # trigger it. Run it here explicitly so this dedicated validation
workflow covers it too.
+ run: |
+ cd dsl/camel-jbang/camel-launcher
+ mvn -B -ntp test -pl .
-Dtest=PackageNativeValidationTest,WebsiteInstallTest
+
+ # ── Windows (install.ps1 unit tests) ──────────────────────────────
+ windows-validator:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #
v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 #
v5.6.0
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ cache: 'maven'
+
+ # camel-repackager-maven-plugin builds the launcher's self-executing
JAR. It is not in the
+ # -pl list below, so without this step Maven resolves it from the Apache
snapshot repository.
+ # This workflow only triggers on dsl/camel-jbang/camel-launcher and
tooling/camel-exe changes
+ # (see the `paths:` filters above), so the plugin's own sources are
never what's under test here.
+ - name: Install camel-repackager-maven-plugin (build-time plugin,
unresolvable from this reactor pass)
+ shell: bash
+ run: |
+ mvn -B -P apache-snapshots -ntp \
+ -pl tooling/maven/camel-repackager-maven-plugin install -DskipTests
+
+ - name: Build camel-launcher module
+ shell: bash
+ # No -am: build only the listed modules. camel-launcher's other
in-repo dependencies
+ # (camel-jbang-core, its jbang plugins, camel-core, ...) resolve as
the current version's
+ # published snapshot instead of being rebuilt from source, which is
what previously turned
+ # this into a ~626-module reactor build. -P apache-snapshots is
required for that resolution.
+ run: |
+ mvn -B -P apache-snapshots -ntp \
+ -pl buildingtools,dsl/camel-jbang/camel-launcher install
-DskipTests
+
+ - name: Run Windows installer unit tests
+ shell: bash
+ run: |
+ cd dsl/camel-jbang/camel-launcher
+ mvn -B -ntp test -pl . -Dtest=WebsiteInstallTest
diff --git a/docs/user-manual/modules/ROOT/nav.adoc
b/docs/user-manual/modules/ROOT/nav.adoc
index 1b8a556c336f..433b538a6dba 100644
--- a/docs/user-manual/modules/ROOT/nav.adoc
+++ b/docs/user-manual/modules/ROOT/nav.adoc
@@ -8,6 +8,7 @@
** xref:camel-jbang.adoc[Camel CLI]
*** xref:jbang-commands/camel-jbang-commands.adoc[Camel CLI Command Reference]
*** xref:camel-jbang-launcher.adoc[Camel CLI Launcher]
+*** xref:camel-jbang-launcher-install.adoc[Installing the Camel CLI Launcher]
*** xref:camel-jbang-kubernetes.adoc[Camel Kubernetes Plugin]
*** xref:camel-jbang-test.adoc[Camel Testing Plugin]
*** xref:camel-jbang-tui.adoc[Camel TUI]
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 48f8e6d52479..82aacf6f0e42 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -178,6 +178,15 @@ Two new commands are available in the
xref:camel-jbang-launcher.adoc[Camel CLI L
See xref:camel-jbang-launcher-install.adoc[Installing the Camel CLI Launcher]
for full details.
+==== Camel CLI package-native validation (maintainers)
+
+The `camel-launcher` module adds an offline package-native validator
+(`src/jreleaser/bin/camel-validate.sh`) that installs each generated package,
asserts
+`camel version` and an offline `camel init`, then uninstalls. Each validator
self-skips
+when its package manager is absent, so the same entry point runs on macOS,
Linux, and
+Windows x64 CI. Validation performs no remote publication and calls no SDKMAN
Vendor
+API; that is the `publish` workflow.
+
=== camel-langchain4j-agent
The `Agent.chat()` method return type has changed from `String` to
`Result<String>` (from `dev.langchain4j.service.Result`).
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher-install.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher-install.adoc
index 3054cc2e16b5..7aae2600a640 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher-install.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher-install.adoc
@@ -25,6 +25,30 @@ paths, `../` traversal, escaping symlinks/reparse points, or
more than one top-l
staged launcher is run once to confirm a Java 17+ runtime can be discovered;
if that check fails,
the previously active installation, if any, is left untouched and the
installer exits nonzero.
+== Installing a specific version
+
+On Linux and macOS, pass the argument through to the piped shell with `-s --`:
+
+[source,bash]
+----
+curl -fsSL https://camel.apache.org/install.sh | sh -s -- --version 4.22.0
+----
+
+On Windows, download the script first, then run it with the parameter:
+
+[source,powershell]
+----
+irm https://camel.apache.org/install.ps1 -OutFile install.ps1
+.\install.ps1 -Version 4.22.0
+----
+
+== Requirements
+
+* A Java 17+ runtime available at install and run time. The launcher locates
Java from
+ `JAVACMD`, `JAVA_HOME`, or `PATH`.
+* Linux and macOS: `curl` or `wget`; `tar`; and one of `sha256sum`, `shasum`,
or `openssl`.
+* Windows: PowerShell 5.1 or later (`Invoke-WebRequest`, `Expand-Archive`,
`Get-FileHash`).
+
== Where the CLI is installed
Installation is always per-user and never requires elevation or `sudo`:
@@ -103,3 +127,30 @@ shell currently runs; the others are unused but still
present.
`camel doctor` exits non-zero when more than one installation is found, so the
check is scriptable
in CI.
+
+== Uninstalling
+
+The installers do not modify shell profiles or the machine `PATH`, so removal
is a matter of
+deleting the per-user files:
+
+On Linux and macOS:
+
+[source,bash]
+----
+rm -f "$HOME/.local/bin/camel"
+rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/camel-cli"
+----
+
+On Windows, remove the install directory and drop its bin directory from your
user `PATH`:
+
+[source,powershell]
+----
+Remove-Item -Recurse -Force "$env:LOCALAPPDATA\Apache Camel"
+----
+
+== See also
+
+* xref:camel-jbang-launcher.adoc[Camel CLI Launcher] — the distribution the
installers install.
+* xref:camel-jbang-installation.adoc[Camel CLI - Installation Options] —
installing via JBang,
+ version pinning, the Camel Wrapper, and the container image.
+* xref:camel-jbang-getting-started.adoc[Getting Started] — the standard
JBang-based install.
diff --git a/dsl/camel-jbang/camel-launcher/jreleaser.yml
b/dsl/camel-jbang/camel-launcher/jreleaser.yml
index da41cb2e610a..8f94c67809f3 100644
--- a/dsl/camel-jbang/camel-launcher/jreleaser.yml
+++ b/dsl/camel-jbang/camel-launcher/jreleaser.yml
@@ -75,7 +75,7 @@
project:
name: camel-cli
- description: Apache Camel CLI
+ description: CLI for building and running Apache Camel integration routes
links:
homepage: https://camel.apache.org/
authors:
@@ -138,8 +138,8 @@ distributions:
deprecateDate: "{{ Env.CAMEL_PKG_BREW_DEPRECATE_DATE }}"
disableDate: "{{ Env.CAMEL_PKG_BREW_DISABLE_DATE }}"
livecheck:
- - 'url
"https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/maven-metadata.xml"'
- - 'regex(/{{{ Env.CAMEL_PKG_BREW_LIVECHECK_REGEX }}}/i)'
+ - 'url
"https://search.maven.org/remotecontent?filepath=org/apache/camel/camel-launcher/maven-metadata.xml"'
+ - '{{{ Env.CAMEL_PKG_BREW_LIVECHECK_REGEX }}}'
sdkman:
active: RELEASE
candidate: camel
diff --git a/dsl/camel-jbang/camel-launcher/pom.xml
b/dsl/camel-jbang/camel-launcher/pom.xml
index a9a6c9e24c27..a8cdcb6db367 100644
--- a/dsl/camel-jbang/camel-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-launcher/pom.xml
@@ -40,6 +40,12 @@
<supportLevel>Stable</supportLevel>
<camel-prepare-component>false</camel-prepare-component>
+ <!-- WebsiteInstallTest's Windows PowerShell suite spawns install.ps1
through a fresh
+ powershell.exe per test (~25 tests); on GitHub's windows-latest
runners each invocation
+ costs ~24s of PowerShell startup/Defender-scan overhead, so the
class alone runs past the
+ default 600s fork budget even though no single test hangs. -->
+ <camel.surefire.forkTimeout>1200</camel.surefire.forkTimeout>
+
<!-- Dependency versions -->
<camel-kamelets-version>4.20.0</camel-kamelets-version>
<maven-version>3.9.16</maven-version>
diff --git a/dsl/camel-jbang/camel-launcher/src/install/install.ps1
b/dsl/camel-jbang/camel-launcher/src/install/install.ps1
index 129323b5382a..552c426a9370 100644
--- a/dsl/camel-jbang/camel-launcher/src/install/install.ps1
+++ b/dsl/camel-jbang/camel-launcher/src/install/install.ps1
@@ -49,33 +49,75 @@ function Test-ValidSha256 {
}
}
+$UseSkipCertificateCheck = $false
if ($CaCertPath) {
# Test seam only: trusts the loopback fixture's self-signed CA for this
process without touching
# the real Windows certificate store. Production installs never set
CAMEL_INSTALL_CA_CERT.
- $installerCaCert = New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2($CaCertPath)
- [System.Net.ServicePointManager]::SecurityProtocol =
[System.Net.SecurityProtocolType]::Tls12
- [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
- param($sender, $certificate, $chain, $sslPolicyErrors)
- $verifyChain = New-Object
System.Security.Cryptography.X509Certificates.X509Chain
- $verifyChain.ChainPolicy.ExtraStore.Add($installerCaCert) | Out-Null
- $verifyChain.ChainPolicy.RevocationMode =
[System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
- $verifyChain.ChainPolicy.VerificationFlags =
[System.Security.Cryptography.X509Certificates.X509VerificationFlags]::AllowUnknownCertificateAuthority
- $leaf = New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2($certificate)
- if (-not $verifyChain.Build($leaf)) {
- return $false
+ if ($PSVersionTable.PSEdition -eq 'Core') {
+ # PowerShell 7+'s Invoke-WebRequest runs on HttpClient, which never
consults
+ # ServicePointManager.ServerCertificateValidationCallback (a Windows
PowerShell/.NET Framework-
+ # only hook) - it's silently ignored, so the callback below never
rejects a bad cert either.
+ # Pin trust via -SkipCertificateCheck on the call instead.
+ $UseSkipCertificateCheck = $true
+ } else {
+ $installerCaCert = New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2($CaCertPath)
+ [System.Net.ServicePointManager]::SecurityProtocol =
[System.Net.SecurityProtocolType]::Tls12
+
+ # ServerCertificateValidationCallback is invoked by the CLR on
whatever thread performs the TLS
+ # handshake - for HttpWebRequest that is often a
thread-pool/IO-completion-port thread with no
+ # PowerShell Runspace bound to it. A scriptblock delegate then fails
intermittently with "There is
+ # no Runspace available to run scripts in this thread." A compiled
type has no such dependency, so
+ # validate via Add-Type instead of a scriptblock.
+ if (-not ('CamelInstallCertValidator' -as [type])) {
+ Add-Type -TypeDefinition @"
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+public static class CamelInstallCertValidator {
+ public static X509Certificate2 CaCert;
+
+ public static bool Validate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors) {
+ using (var verifyChain = new X509Chain()) {
+ verifyChain.ChainPolicy.ExtraStore.Add(CaCert);
+ verifyChain.ChainPolicy.RevocationMode =
X509RevocationMode.NoCheck;
+ verifyChain.ChainPolicy.VerificationFlags =
X509VerificationFlags.AllowUnknownCertificateAuthority;
+ var leaf = new X509Certificate2(certificate);
+ if (!verifyChain.Build(leaf)) {
+ return false;
+ }
+ var root =
verifyChain.ChainElements[verifyChain.ChainElements.Count - 1].Certificate;
+ return root.Thumbprint == CaCert.Thumbprint;
+ }
+ }
+}
+"@
}
- $root = $verifyChain.ChainElements[$verifyChain.ChainElements.Count -
1].Certificate
- return $root.Thumbprint -eq $installerCaCert.Thumbprint
- }.GetNewClosure()
+ [CamelInstallCertValidator]::CaCert = $installerCaCert
+ # Windows PowerShell 5.1 cannot implicitly convert a bare method
reference (PSMethod) to a
+ # custom delegate type - "Cannot convert ... value of type
System.Management.Automation.PSMethod
+ # to type System.Net.Security.RemoteCertificateValidationCallback."
Build the delegate explicitly.
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback
= [System.Delegate]::CreateDelegate(
+ [System.Net.Security.RemoteCertificateValidationCallback],
+ [CamelInstallCertValidator],
+ 'Validate')
+ }
}
# Downloads $Url to $OutFile; used for both the manifest and archive fetches.
function Save-RemoteFile {
param([string] $Url, [string] $OutFile)
try {
- Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing |
Out-Null
+ if ($UseSkipCertificateCheck) {
+ Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing
-SkipCertificateCheck | Out-Null
+ } else {
+ Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing |
Out-Null
+ }
} catch {
- Fail "failed to download $Url"
+ $detail = $_.Exception.Message
+ if ($_.Exception.InnerException) {
+ $detail = "$detail -- $($_.Exception.InnerException.Message)"
+ }
+ Fail "failed to download $Url ($detail)"
}
}
@@ -157,7 +199,11 @@ function Test-ArchiveEntry {
if ($segments -contains '..') {
Fail "archive contains a path traversal entry: $name"
}
- $unixMode = ([uint32]$entry.ExternalAttributes -shr 16) -band
0xF000
+ # ExternalAttributes is a signed Int32 whose upper 16 bits carry
the Unix mode; any entry
+ # with mode bits set (e.g. 0100644, universal for a POSIX-built
archive) overflows into
+ # negative range there. [uint32] is a checked cast that throws on
a negative input, so mask
+ # to the low 32 bits as Int64 first to reinterpret the same bit
pattern unsigned.
+ $unixMode = ([uint32]($entry.ExternalAttributes -band 0xFFFFFFFFL)
-shr 16) -band 0xF000
if ($unixMode -eq 0xA000) {
Fail "archive contains a symbolic link or reparse point entry,
which is not allowed"
}
@@ -205,8 +251,13 @@ function Set-CamelShim {
Move-Item -LiteralPath $StagedRoot -Destination $targetDir
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
- $launcherPath = Join-Path $targetDir 'bin\camel.bat'
- $shimContent = "@echo off`r`ncall `"$launcherPath`" %*`r`nexit /b
%ERRORLEVEL%`r`n"
+ # Resolve the launcher relative to the shim's own location (%~dp0 =
<InstallRoot>\bin\) rather than
+ # embedding the absolute install path as literal text. cmd.exe parses
batch-file bytes in the OEM
+ # code page, so a non-ASCII install path (e.g. a user profile containing
'über') written into the
+ # file as UTF-8 would be misread and 'call' would fail with "The system
cannot find the path
+ # specified." %~dp0 supplies that path as a runtime value instead, which
cmd handles correctly - the
+ # same self-location pattern camel.bat itself uses. Only the version
(validated ASCII) is embedded.
+ $shimContent = "@echo off`r`ncall
`"%~dp0..\cli\versions\$Version\bin\camel.bat`" %*`r`nexit /b %ERRORLEVEL%`r`n"
$tempShim = Join-Path $BinDir ".camel.$PID.tmp.cmd"
# Write without a BOM: Windows PowerShell 5.1's 'Set-Content -Encoding
UTF8' prepends one, which
# cmd.exe treats as part of the first line, breaking '@echo off' and
emitting a stray error.
@@ -311,4 +362,7 @@ try {
if (Test-Path -LiteralPath $stagingRoot) {
Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction
SilentlyContinue
}
+ # ServerCertificateValidationCallback is process-wide static state; clear
it even though this
+ # process is about to exit, in case a caller dot-sources install.ps1 into
a longer-lived session.
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback =
$null
}
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 8057a667ab54..4ec875618ca3 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
@@ -111,7 +111,11 @@ if [ "$CHANNEL" = "stable" ]; then
WEBSITE_LATEST="true"
# The maven-metadata.xml <release> tag always reflects the newest published
release across every
# line, which is exactly "latest stable" for this formula - no line-prefix
filtering needed.
- CAMEL_PKG_BREW_LIVECHECK_REGEX='<release>([0-9]+\.[0-9]+\.[0-9]+)<\/release>'
+ # %r{} (not /.../ ) per Homebrew's Style/RegexpLiteral cop, which the
pattern's escaped "/"
+ # would otherwise trip; wrapped here (not in jreleaser.yml) because
livecheck's Mustache tag
+ # must stay a clean, unpadded {{{ }}} - butting a literal %r{ or trailing
}i) directly against
+ # the triple-mustache braces confuses the tag scanner and silently drops the
substitution.
+
CAMEL_PKG_BREW_LIVECHECK_REGEX='regex(%r{<release>([0-9]+\.[0-9]+\.[0-9]+)</release>}i)'
[ -n "$LTS_LINE" ] && BREW_LTS_FORMULA="apache-camel@$LTS_LINE"
else
# Homebrew's own versioned-formula convention names the *file*
"[email protected]" but the
@@ -135,7 +139,7 @@ else
# literally, not "any character") or it would report the project's overall
latest release
# instead of this line's own latest patch.
lts_line_escaped=$(echo "$LTS_LINE" | sed 's/\./\\./g')
-
CAMEL_PKG_BREW_LIVECHECK_REGEX="<version>(${lts_line_escaped}\.[0-9]+)<\/version>"
+
CAMEL_PKG_BREW_LIVECHECK_REGEX="regex(%r{<version>(${lts_line_escaped}\.[0-9]+)</version>}i)"
# deprecate! fires 6 months before the line's own documented support end;
disable! fires on the
# support-end date itself. supported_ends was already resolved and validated
(non-empty, a real
# entry in supported-lts.yml) by the --lts-line validation earlier in this
script - re-used here
diff --git a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-validate.sh
b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-validate.sh
index 0d13879bb472..1566fdba0c2c 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-validate.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/camel-validate.sh
@@ -23,22 +23,38 @@
# camel-validate.sh <command> [options]
#
# Commands:
-# all Run all available validators (homebrew, sdkman). Each is
host-gated.
+# all Run every validator: local (always, if the archive is present),
plus the
+# host-gated homebrew and sdkman validators.
+# local Validate this build's own archive directly (no package manager
involved)
# homebrew Validate via Homebrew (audit + install + version + init +
uninstall)
-# sdkman Validate via SDKMAN (descriptor check + offline archive + version
+ init)
+# sdkman Validate via SDKMAN (descriptor + archive checks only;
version/init are not
+# exercised offline - see validate_sdkman)
# help Show usage
#
# Host-gating:
-# Each validator checks for its package manager. If absent, prints "SKIP:"
and exits 0.
-# If present but a failure occurs, exits nonzero.
+# The homebrew and sdkman validators check for their package manager and
print "SKIP:"
+# (exit 0) if it is absent. The local validator is not
package-manager-gated; it runs
+# whenever the staged archive exists. Any genuine validation failure exits
nonzero.
# ============================================================================
set -eu
-SCRIPT_DIR=`CDPATH= cd -- "$(dirname -- "$0")" && pwd`
-MODULE_DIR=`CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd`
+SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
+MODULE_DIR=$(CDPATH='' cd -- "$SCRIPT_DIR/../../.." && pwd)
LIB_DIR="$SCRIPT_DIR/lib"
+# Prepare/build outputs live under target/. Overridable in test mode only, so
unit tests can
+# point the archive validators at a synthetic staging dir instead of the real
target/.
+if [ -n "${CAMEL_VALIDATE_TARGET_DIR:-}" ]; then
+ if [ "${CAMEL_PACKAGE_TEST_MODE:-}" != "true" ]; then
+ echo "Error: CAMEL_VALIDATE_TARGET_DIR requires
CAMEL_PACKAGE_TEST_MODE=true." >&2
+ exit 2
+ fi
+ TARGET_DIR="$CAMEL_VALIDATE_TARGET_DIR"
+else
+ TARGET_DIR="$MODULE_DIR/target"
+fi
+
usage() {
cat <<EOF
Usage: camel-validate.sh <command> [options]
@@ -48,7 +64,8 @@ Commands:
validators are host-gated; "local" always runs if the archive is
present.
local Validate this build's own archive directly (no package manager
involved)
homebrew Validate via Homebrew (audit + install + version + init +
uninstall)
- sdkman Validate via SDKMAN (descriptor check + offline archive + version
+ init)
+ sdkman Validate via SDKMAN (descriptor + archive checks only;
version/init are not
+ exercised offline)
help Show this help message
Options:
@@ -88,12 +105,22 @@ done
case "$COMMAND" in help) usage ;; esac
+case "$CHANNEL" in
+ stable|lts) ;;
+ *) echo "Error: --channel must be 'stable' or 'lts' (got '$CHANNEL')." >&2;
usage ;;
+esac
+
+if [ "$CHANNEL" = "lts" ] && [ -z "$LTS_LINE" ]; then
+ echo "Error: --channel lts requires --lts-line X.Y." >&2
+ usage
+fi
+
# ---------- sourcing ----------
. "$LIB_DIR/assert-camel-cli.sh"
# ---------- shared temp workspace ----------
-# Used by every validator (homebrew, sdkman); created once and cleaned on
exit. Kept at script
+# Used by every validator (local, homebrew, sdkman); created once and cleaned
on exit. Kept at script
# scope (not local to a validator) so both functions and the EXIT trap can see
it under 'set -u'.
TMPDIR_BASE=$(mktemp -d)
@@ -140,6 +167,17 @@ formula_name() {
esac
}
+# Portable sha256 of a file (stdout = bare hex digest). Mirrors install.sh's
fallback chain.
+sha256_of() {
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$1" | awk '{print $1}'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{print $1}'
+ else
+ openssl dgst -sha256 "$1" | awk '{print $NF}'
+ fi
+}
+
# ============================================================
# Homebrew validator
# ============================================================
@@ -159,7 +197,7 @@ validate_homebrew() {
# target/jreleaser/package/camel-cli/brew/Formula/<formula-name>.rb
# (empirically verified with a real, non-stubbed jreleaser:package dry
run; there is no
# target/jreleaser/distributions/ directory at all in this JReleaser
version).
-
formula_file="$MODULE_DIR/target/jreleaser/package/camel-cli/brew/Formula/${fmla}.rb"
+
formula_file="$TARGET_DIR/jreleaser/package/camel-cli/brew/Formula/${fmla}.rb"
if [ ! -f "$formula_file" ]; then
echo "SKIP: homebrew formula not found: $formula_file (did prepare
run?)"
@@ -186,28 +224,68 @@ validate_homebrew() {
BREW_TAP_NAME="$tap_name"
tap_dir=$(brew --repository "$tap_name")
- cp -p "$formula_file" "$tap_dir/Formula/${fmla}.rb"
- chmod a+r "$tap_dir/Formula/${fmla}.rb"
+ if ! cp -p "$formula_file" "$tap_dir/Formula/${fmla}.rb"; then
+ echo "FAIL: could not copy the generated formula into the validation
tap"
+ return 1
+ fi
+ # 644, not `a+r`: a world-writable formula file trips `brew audit --strict`
+ # (FormulaAudit/Files "Incorrect file permissions (777)"), which would be
a false
+ # failure of the audit gate below caused by the tap copy itself, not by
the formula.
+ chmod 644 "$tap_dir/Formula/${fmla}.rb"
# Step 1: Homebrew style + audit, referenced by tap-qualified name (not a
path).
- local audit_output=""
- audit_output=$(HOMEBREW_NO_AUTO_UPDATE=1 brew style --fix
"$tap_name/$fmla" 2>&1 || true)
+ # `brew style --fix` only normalizes formatting; its output is
informational.
+ local style_output=""
+ style_output=$(HOMEBREW_NO_AUTO_UPDATE=1 brew style --fix
"$tap_name/$fmla" 2>&1 || true)
echo "INFO: homebrew style output:"
+ echo "$style_output"
+
+ # `brew audit --strict` is a real gate: a nonzero exit means a defect in
the generated
+ # formula and must fail validation, not warn past it. --online is
deliberately omitted so
+ # audit stays offline and never fails merely because the (test-mode,
unpublished) download
+ # URL is unreachable - only the offline structural/style/naming checks run
here.
+ local audit_output audit_rc=0
+ audit_output=$(HOMEBREW_NO_AUTO_UPDATE=1 brew audit --strict
"$tap_name/$fmla" 2>&1) || audit_rc=$?
+ # Always echo the audit output (even on success) so the log shows the gate
actually ran and
+ # surfaces any advisory lines that did not rise to a nonzero exit.
+ echo "INFO: homebrew audit --strict output:"
echo "$audit_output"
-
- # --new (not just --strict): homebrew-core enforces additional checks
specific to a first-time
- # formula submission - the bar apache-camel needs to clear before
homebrew-core will accept it.
- # Plain --strict is the bar for an *already-accepted* formula being
updated, which understates
- # what's needed for this PR.
- audit_output=$(HOMEBREW_NO_AUTO_UPDATE=1 brew audit --new --strict
"$tap_name/$fmla" 2>&1 || true)
- if echo "$audit_output" | grep -Eqi "Error|error:"; then
- echo "WARN: homebrew audit reported issues:"
- echo "$audit_output"
+ if [ "$audit_rc" -ne 0 ]; then
+ echo "FAIL: homebrew audit --strict reported issues (exit $audit_rc)"
+ rc=1
else
echo "PASS: homebrew style/audit passed"
fi
- # Step 2: Install the formula from the tap
+ # Step 2: Install the formula from the tap.
+ #
+ # In test mode the formula's download URL points at a Maven coordinate for
a version that
+ # is not published yet (in CI the real POM version is a -SNAPSHOT, and
JReleaser renders
+ # the URL from that POM version - Central never hosts SNAPSHOTs). Rather
than let `brew
+ # install` fail to download and skip the whole
install/version/init/uninstall chain,
+ # rewrite the tapped formula's url to a file:// path for this build's own
staged archive
+ # and recompute its sha256, so brew genuinely downloads, verifies,
extracts and installs
+ # the real binary this build produced - fully offline and deterministic.
Real release runs
+ # (not test mode) keep the published Central URL untouched. This runs
after `brew audit`
+ # above, so audit still validates the real published-style formula, not
the file:// one.
+ if [ "${CAMEL_PACKAGE_TEST_MODE:-}" = "true" ]; then
+ local url_basename staged_archive staged_sha
+ url_basename=$(sed -n 's/^[[:space:]]*url "[^"]*\/\([^/"]*\)"/\1/p'
"$tap_dir/Formula/${fmla}.rb" | head -n1)
+ staged_archive="$TARGET_DIR/$url_basename"
+ if [ -z "$url_basename" ] || [ ! -f "$staged_archive" ]; then
+ echo "FAIL: test-mode formula rewrite could not locate the staged
archive for '$url_basename' under $TARGET_DIR"
+ return 1
+ fi
+ staged_sha=$(sha256_of "$staged_archive")
+ if ! sed -e "s|^\([[:space:]]*\)url \"[^\"]*\"|\1url
\"file://$staged_archive\"|" \
+ -e "s|^\([[:space:]]*\)sha256 \"[^\"]*\"|\1sha256
\"$staged_sha\"|" \
+ "$tap_dir/Formula/${fmla}.rb" >
"$tap_dir/Formula/${fmla}.rb.new"; then
+ echo "FAIL: could not rewrite the tapped formula for offline
install"
+ return 1
+ fi
+ mv -f "$tap_dir/Formula/${fmla}.rb.new" "$tap_dir/Formula/${fmla}.rb"
+ fi
+
local BREW_HOME="$TMPDIR_BASE/brew-home"
mkdir -p "$BREW_HOME"
local brew_output install_rc=0
@@ -219,21 +297,23 @@ validate_homebrew() {
if echo "$brew_output" | grep -Eqi "no suitable java"; then
# A missing/incompatible Java dependency is a host-resolution
limitation, not a
# defect in the generated formula; nothing was installed, so there
is nothing
- # left to validate.
+ # left to validate. Still propagate $rc: an audit failure recorded
above is a real
+ # defect and must not be masked just because install itself
couldn't proceed.
echo "SKIP: homebrew install could not resolve a Java dependency
on this host:"
echo "$brew_output"
- return 0
+ return "$rc"
fi
- if echo "$brew_output" | grep -Eqi "Failed to download resource|curl:
\("; then
- # The formula's download URL always points at the real Maven
Central release
- # coordinates (see jreleaser.yml); for a local/offline or
synthetic-version
- # validation run, no artifact was ever actually published there,
so the fetch
- # can never succeed here regardless of whether the generated
formula is
- # correct. Host/environment limitation, not a defect in the
formula itself;
- # empirically confirmed on this machine.
- echo "SKIP: homebrew install could not download the release
artifact (not published for this version, expected for local/offline
validation):"
+ if [ "${CAMEL_PACKAGE_TEST_MODE:-}" != "true" ] \
+ && echo "$brew_output" | grep -Eqi "Failed to download
resource|curl: \("; then
+ # Non-test (real release) runs only: if the release artifact is
not yet published at
+ # its Central coordinate, the fetch cannot succeed regardless of
whether the formula
+ # is correct, so treat it as an environment limitation. In test
mode the url was
+ # rewritten to a local file:// archive above, so a download
failure there is a
+ # genuine defect and falls through to FAIL below. Propagate $rc
for the same reason
+ # as the Java-dependency SKIP above: an already-recorded audit
failure must survive.
+ echo "SKIP: homebrew install could not download the release
artifact (not published yet, expected for an unpublished release):"
echo "$brew_output"
- return 0
+ return "$rc"
fi
echo "FAIL: homebrew install failed (exit $install_rc):"
echo "$brew_output"
@@ -245,16 +325,18 @@ validate_homebrew() {
# is not proof the executable actually works, so a
missing/empty/mismatched result here
# is a real failure, not something to warn past.
#
- # Expected version comes from the formula's own `version` line, not
$RESOLVED_VERSION:
- # camel-package.sh's test-mode-only hack patches url/version/sha256 to a
real,
- # already-published camel-launcher release so `brew install` can genuinely
download
- # and verify it (see the comment in camel-package.sh), so what actually
gets installed
- # here may differ from this build's own synthetic version. Reading it back
out of the
- # tapped formula keeps this assertion correct in both that case and real
releases
- # (where it's simply $RESOLVED_VERSION again).
+ # Expected version comes from the formula's own `version` line, not
$RESOLVED_VERSION.
+ # JReleaser renders the formula version from the real POM version, which
in test mode is
+ # the -SNAPSHOT that the offline file:// install above actually installs;
that differs from
+ # $RESOLVED_VERSION (which strips -SNAPSHOT for local-archive lookups).
Reading the expected
+ # value back out of the tapped formula keeps this assertion correct in
both the test-mode
+ # case and a real release (where the formula version is $RESOLVED_VERSION
anyway).
local expected_version
expected_version=$(sed -n 's/^[[:space:]]*version "\(.*\)"/\1/p'
"$tap_dir/Formula/${fmla}.rb" | head -n1)
- [ -n "$expected_version" ] || expected_version="$RESOLVED_VERSION"
+ if [ -z "$expected_version" ]; then
+ echo "WARN: could not read 'version' from tapped formula ${fmla}.rb,
falling back to \$RESOLVED_VERSION ($RESOLVED_VERSION)"
+ expected_version="$RESOLVED_VERSION"
+ fi
local camv_output=""
if ! command -v camel >/dev/null 2>&1; then
@@ -263,7 +345,9 @@ validate_homebrew() {
else
camv_output=$(camel --version 2>/dev/null) || true
if [ -z "$camv_output" ]; then
- echo "FAIL: 'camel --version' returned empty output after install"
+ local camv_err=""
+ camv_err=$(camel --version 2>&1 >/dev/null) || true
+ echo "FAIL: 'camel --version' returned empty output after
install${camv_err:+ (stderr: $camv_err)}"
rc=1
else
assert_camel_version "$camv_output" "$expected_version" && \
@@ -277,30 +361,43 @@ validate_homebrew() {
if ! command -v camel >/dev/null 2>&1; then
echo "FAIL: camel not available for init test (install reported
success but binary is missing)"
rc=1
- elif ! (cd "$init_dir" && camel init hello.java >/dev/null 2>&1); then
- echo "FAIL: camel init failed after a successful homebrew install"
- rc=1
else
- assert_init_content "$init_dir" "hello.java" || rc=1
+ local init_err=""
+ if ! init_err=$(cd "$init_dir" && camel init hello.java 2>&1
>/dev/null); then
+ echo "FAIL: camel init failed after a successful homebrew
install${init_err:+: $init_err}"
+ rc=1
+ else
+ assert_init_content "$init_dir" "hello.java" || rc=1
+ fi
fi
# Step 5: Uninstall (tap-qualified name, matching the install above; the
tap itself is
# removed afterward by the script's EXIT trap, once nothing installed from
it remains).
- local uninstall_rc=0
- if command -v brew >/dev/null 2>&1; then
- local brew_uninst_output=""
- brew_uninst_output=$(HOME="$BREW_HOME" \
- HOMEBREW_NO_AUTO_UPDATE=1 \
- brew uninstall "$tap_name/$fmla" --force 2>&1 || true)
- echo "INFO: homebrew uninstall output:"
- echo "$brew_uninst_output"
- fi
-
- # Verify removal of symlink/bin entry
- if [ -L "/usr/local/bin/camel" ] || [ -e "/usr/local/bin/camel" ]; then
- echo "WARN: homebrew uninstall left /usr/local/bin/camel"
+ # Capture where the installed `camel` actually resolves BEFORE
uninstalling, so the
+ # post-uninstall check targets Homebrew's real prefix (e.g.
/opt/homebrew/bin on Apple
+ # Silicon, /home/linuxbrew/.linuxbrew/bin on Linux) rather than a
hardcoded path that is
+ # never populated on either CI runner.
+ local installed_camel=""
+ installed_camel=$(command -v camel 2>/dev/null || true)
+
+ local brew_uninst_output=""
+ brew_uninst_output=$(HOME="$BREW_HOME" \
+ HOMEBREW_NO_AUTO_UPDATE=1 \
+ brew uninstall "$tap_name/$fmla" --force 2>&1) || {
+ echo "FAIL: homebrew uninstall command failed:"
+ echo "$brew_uninst_output"
+ rc=1
+ }
+ echo "INFO: homebrew uninstall output:"
+ echo "$brew_uninst_output"
+
+ # A binary still resolvable at its install location means uninstall did
not remove it -
+ # a real failure, not a warning.
+ if [ -n "$installed_camel" ] && { [ -e "$installed_camel" ] || [ -L
"$installed_camel" ]; }; then
+ echo "FAIL: homebrew uninstall left the camel executable at
$installed_camel"
+ rc=1
else
- echo "PASS: homebrew uninstall removed camel executable"
+ echo "PASS: homebrew uninstall removed the camel executable"
fi
return $rc
@@ -309,15 +406,15 @@ validate_homebrew() {
# ============================================================
# Local archive validator (no package manager involved)
# ============================================================
-# The Homebrew validator's test-mode hack (see camel-package.sh) can install a
real,
-# already-published camel-launcher release instead of this build's own
synthetic one, so
-# it no longer guarantees this build's own zip/tar.gz ever actually gets run.
This
-# validator always runs, independent of any package manager, so that coverage
isn't lost:
-# it extracts the locally staged archive directly and runs its bin/camel.sh in
place.
+# Runs this build's own archive directly, with no package manager in the loop.
Even though the
+# Homebrew validator now installs this build's own binary via a file://
rewrite in test mode,
+# this validator is still the one leg guaranteed to run regardless of whether
Homebrew or SDKMAN
+# is present on the host: it extracts the locally staged archive and runs its
bin/camel.sh in
+# place.
validate_local_archive() {
local rc=0
- local
archive_file="$MODULE_DIR/target/camel-launcher-${RESOLVED_VERSION}-bin.tar.gz"
+ local
archive_file="$TARGET_DIR/camel-launcher-${RESOLVED_VERSION}-bin.tar.gz"
if [ ! -f "$archive_file" ]; then
echo "SKIP: local archive not found: $archive_file (did the build
run?)"
@@ -344,17 +441,33 @@ validate_local_archive() {
local camv_output=""
camv_output=$("$camel_sh" --version 2>/dev/null) || true
if [ -z "$camv_output" ]; then
- echo "FAIL: 'camel.sh --version' returned empty output"
+ local camv_err=""
+ camv_err=$("$camel_sh" --version 2>&1 >/dev/null) || true
+ echo "FAIL: 'camel.sh --version' returned empty output${camv_err:+
(stderr: $camv_err)}"
rc=1
else
- assert_camel_version "$camv_output" "$RESOLVED_VERSION" && \
- echo "PASS: local archive version OK" || rc=1
+ # In test mode this archive can be a byte-for-byte copy of a real
SNAPSHOT build, staged
+ # under a -SNAPSHOT-stripped filename (see the CI workflow's "stage a
synthetic release
+ # version" step, which `cp`s the archive rather than rebuilding it).
The binary's embedded
+ # catalog version is whatever the real build produced, so it genuinely
reports
+ # "$RESOLVED_VERSION-SNAPSHOT" there, not "$RESOLVED_VERSION" -
confirmed empirically
+ # against a real build. A non-test release never runs in a SNAPSHOT
state, so only test
+ # mode accepts the suffixed form.
+ local actual_ver
+ actual_ver=$(echo "$camv_output" | head -n 1 | awk '{print $NF}')
+ if [ "${CAMEL_PACKAGE_TEST_MODE:-}" = "true" ] && [ "$actual_ver" =
"${RESOLVED_VERSION}-SNAPSHOT" ]; then
+ echo "PASS: local archive version OK (test-mode staged archive
reports its real build version '$actual_ver')"
+ else
+ assert_camel_version "$camv_output" "$RESOLVED_VERSION" && \
+ echo "PASS: local archive version OK" || rc=1
+ fi
fi
local init_dir="$TMPDIR_BASE/local-archive-init"
mkdir -p "$init_dir"
- if ! (cd "$init_dir" && "$camel_sh" init hello.java >/dev/null 2>&1); then
- echo "FAIL: camel init failed against the locally extracted archive"
+ local init_err=""
+ if ! init_err=$(cd "$init_dir" && "$camel_sh" init hello.java 2>&1
>/dev/null); then
+ echo "FAIL: camel init failed against the locally extracted
archive${init_err:+: $init_err}"
rc=1
else
assert_init_content "$init_dir" "hello.java" || rc=1
@@ -379,7 +492,7 @@ validate_sdkman() {
# API-only and writes no local descriptor file under target/jreleaser at
all, so this always
# SKIPs offline regardless of the path below; kept for parity with the
Homebrew validator and
# in case a future JReleaser version starts writing one.
- local
DESCRIPTOR_FILE="$MODULE_DIR/target/jreleaser/package/camel-cli/sdkman/camel.json"
+ local
DESCRIPTOR_FILE="$TARGET_DIR/jreleaser/package/camel-cli/sdkman/camel.json"
if [ ! -f "$DESCRIPTOR_FILE" ]; then
echo "SKIP: SDKMAN descriptor not found: $DESCRIPTOR_FILE (did prepare
run?)"
@@ -396,7 +509,10 @@ validate_sdkman() {
if command -v jq >/dev/null 2>&1; then
local desc_version
desc_version=$(jq -r '.version' "$DESCRIPTOR_FILE" 2>/dev/null) || true
- if [ -z "$desc_version" ]; then
+ # jq prints the literal string "null" (not empty) when the key is
absent, so that
+ # must be checked explicitly - otherwise a descriptor missing
'version' would still
+ # print a false "PASS: ... valid version: null".
+ if [ -z "$desc_version" ] || [ "$desc_version" = "null" ]; then
echo "FAIL: SDKMAN descriptor missing 'version' field"
return 1
fi
@@ -412,7 +528,7 @@ validate_sdkman() {
fi
# Step 2: Verify offline archive structure (tar.gz)
- local
ARCHIVE_FILE="$MODULE_DIR/target/camel-launcher-${RESOLVED_VERSION}-bin.tar.gz"
+ local
ARCHIVE_FILE="$TARGET_DIR/camel-launcher-${RESOLVED_VERSION}-bin.tar.gz"
if [ ! -f "$ARCHIVE_FILE" ]; then
echo "SKIP: SDKMAN archive not found: $ARCHIVE_FILE (did prepare run?)"
return 0
@@ -432,19 +548,27 @@ validate_sdkman() {
return 1
fi
- # Step 3: Verify camel version
- # For offline validation, we verify the descriptor + archive integrity.
- # A real sdk install would call the SDKMAN Vendor Release API which is
stubbed in Phase 5.
- echo "SKIP: SDKMAN vendor release API stubbed (offline validation)"
-
- # Step 4: Verify offline camel init route content (same assertion as
Homebrew)
+ # Step 3: Verify camel version.
+ # For offline validation, we verify the descriptor + archive integrity. A
real `sdk install`
+ # would call the SDKMAN Vendor Release API, which requires a published
release and network
+ # access, so it is not exercised in this offline validation.
+ echo "SKIP: SDKMAN vendor release API not exercised (offline validation)"
+
+ # Step 4: Verify offline camel init route content (same assertion as
Homebrew).
+ # Unlike the Homebrew/local validators, a failing `camel init` here stays
a WARN rather
+ # than a FAIL: SDKMAN never actually installs anything in this offline
validation (see
+ # Step 3 above), so any `camel` found on PATH is unrelated host state this
validator did
+ # not produce - treating its failure as a defect in the SDKMAN packaging
would be a false
+ # failure. A successful init IS still checked against the content fixture
below, since
+ # that's a legitimate signal when it happens to be available.
local init_dir="$TMPDIR_BASE/init-test-sdkman"
mkdir -p "$init_dir"
if command -v camel >/dev/null 2>&1; then
- if cd "$init_dir" && camel init hello.java >/dev/null 2>&1; then
+ local init_err=""
+ if init_err=$(cd "$init_dir" && camel init hello.java 2>&1
>/dev/null); then
assert_init_content "$init_dir" "hello.java" || rc=1
else
- echo "WARN: camel init skipped (not installed via SDKMAN)"
+ echo "WARN: camel init skipped (not installed via
SDKMAN)${init_err:+ (stderr: $init_err)}"
fi
else
echo "WARN: camel not available for init test via SDKMAN (skipped)"
@@ -463,9 +587,9 @@ validate_sdkman() {
main() {
local rc=0
case "$COMMAND" in
- local) validate_local_archive; rc=$? ;;
- homebrew) validate_homebrew; rc=$? ;;
- sdkman) validate_sdkman; rc=$? ;;
+ local) validate_local_archive || rc=1 ;;
+ homebrew) validate_homebrew || rc=1 ;;
+ sdkman) validate_sdkman || rc=1 ;;
all)
validate_local_archive || rc=1
validate_homebrew || rc=1
diff --git
a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/lib/assert-camel-cli.sh
b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/lib/assert-camel-cli.sh
index df2335506b4e..624c996e59bb 100755
--- a/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/lib/assert-camel-cli.sh
+++ b/dsl/camel-jbang/camel-launcher/src/jreleaser/bin/lib/assert-camel-cli.sh
@@ -20,11 +20,12 @@
# Shared POSIX assertions for camel-validate.sh / assertion library tests
#
# Usage (caller must source this file):
-# assert_camel_version <actual_output> <expected_version> -- exits 1 on
mismatch
+# assert_camel_version <actual_output> <expected_version> -- returns 1 on
mismatch
# assert_init_content <directory> <filename> -- verifies
content matches fixture
-# assert_uninstalled <path_or_prefix ...> -- each must
not exist after uninstall
+# assert_uninstalled <path ...> -- each exact
path must not exist after
+# uninstall
(not called by camel-validate.sh's
+# own
validators, which inline this check)
# assert_camel_cli <camel-cmd> <workdir> [expected-version] -- full
version+init assertion wrapper
-# assert_camel_absent <path> -- verify a
camel symlink/entry is gone
# ============================================================================
if [ -n "${BASH_SOURCE+set}" ]; then
@@ -36,14 +37,14 @@ else
fi
if [ -n "$_raw_source" ]; then
- _ASSERT_LIB_DIR=$(CDPATH= cd -- "$(dirname -- "$_raw_source")" && pwd)
+ _ASSERT_LIB_DIR=$(CDPATH='' cd -- "$(dirname -- "$_raw_source")" && pwd)
else
- _ASSERT_LIB_DIR=$(CDPATH= cd -- "$(dirname -- "${0:-.}")" && pwd)
+ _ASSERT_LIB_DIR=$(CDPATH='' cd -- "$(dirname -- "${0:-.}")" && pwd)
fi
unset _raw_source
assertion_pass() { echo "PASS: $1"; }
-assertion_fail() { echo "FAIL: $1"; assertion_error=1; return 1; }
+assertion_fail() { echo "FAIL: $1"; return 1; }
assert_camel_version() {
local actual="$1" expected="$2"
@@ -98,45 +99,45 @@ assert_camel_cli() {
local _orig_dir
_orig_dir=$(pwd)
- # Step 1: version check
+ # Step 1: version check. When an expected version is given, a mismatch is
a real failure
+ # that must propagate out of this wrapper (empty output stays a soft skip
- the caller may
+ # be validating a CLI that cannot print a version on this host).
local camv_output
camv_output=$("$CAMELCMD" --version 2>/dev/null) || true
if [ -z "$camv_output" ]; then
- echo "WARN: camel version returned empty output (skipped)"
+ local camv_err=""
+ camv_err=$("$CAMELCMD" --version 2>&1 >/dev/null) || true
+ echo "WARN: camel version returned empty output (skipped)${camv_err:+
(stderr: $camv_err)}"
elif [ -n "$EXPECTED_VERSION" ]; then
- assert_camel_version "$camv_output" "$EXPECTED_VERSION"
+ assert_camel_version "$camv_output" "$EXPECTED_VERSION" || return 1
else
echo "INFO: camel version reported: $camv_output (no expected version
given, skipping comparison)"
fi
# Step 2: init content check. Uses "hello.java" (not an arbitrary name)
because the
- # fixture's class name is derived from this filename, same as the real
POSIX validators.
+ # fixture's class name is derived from this filename, same as the real
POSIX validators. A
+ # failing init is a genuine defect, not something to warn past - matches
the inline validators
+ # in camel-validate.sh, which FAIL on the same condition.
cd "$WORKDIR" || { echo "FAIL: cannot cd to $WORKDIR"; return 1; }
if [ -f hello.java ]; then rm -f hello.java; fi
- if "$CAMELCMD" init hello.java >/dev/null 2>&1; then
- assert_init_content "$WORKDIR" "hello.java" || { echo "FAIL: generated
route missing expected content"; cd "$_orig_dir"; return 1; }
+ local init_err=""
+ if init_err=$("$CAMELCMD" init hello.java 2>&1 >/dev/null); then
+ assert_init_content "$WORKDIR" "hello.java" || { echo "FAIL: generated
route missing expected content"; cd "$_orig_dir" || exit; return 1; }
else
- echo "WARN: camel init failed (skipped)"
+ echo "FAIL: camel init failed${init_err:+: $init_err}"
+ cd "$_orig_dir" || exit
+ return 1
fi
- cd "$_orig_dir"
+ cd "$_orig_dir" || exit
- # Step 3: assert the executable exists
+ # Step 3: final sanity check that the resolved command is executable,
independent of how
+ # the version/init calls above happened to succeed.
if [ -x "$CAMELCMD" ]; then
assertion_pass "camel CLI executable found"
else
- echo "WARN: camel CLI not executable at '$CAMELCMD' (skipped)"
+ echo "FAIL: camel CLI not executable at '$CAMELCMD'"
+ return 1
fi
return 0
}
-
-assert_camel_absent() {
- local path="$1"
- if [ -e "$path" ] || [ -L "$path" ]; then
- assertion_fail "uninstall left behind: $path"
- return 1
- else
- assertion_pass "'$path' does not exist (removed by uninstall)"
- return 0
- fi
-}
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackageNativeValidationTest.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackageNativeValidationTest.java
new file mode 100644
index 000000000000..202cb73d5e8f
--- /dev/null
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackageNativeValidationTest.java
@@ -0,0 +1,525 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.launcher;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+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.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Host-gated orchestration tests for the POSIX package-native validator
dispatcher and assertion library. Runs on macOS
+ * and Linux (disabled on Windows).
+ */
+@DisabledOnOs(OS.WINDOWS)
+class PackageNativeValidationTest {
+
+ private static final Path MODULE_DIR = Path.of("src").toAbsolutePath();
+ private static final Path LIB =
MODULE_DIR.resolve("jreleaser/bin/lib/assert-camel-cli.sh");
+ private static final Path VALIDATE =
MODULE_DIR.resolve("jreleaser/bin/camel-validate.sh");
+ private static final Path FIXTURE =
MODULE_DIR.resolve("test/resources/validate/expected-init-route.txt");
+
+ private static final String VERSION = "4.22.0";
+
+ @Test
+ void assertionLibraryPassesForConformantCli(@TempDir Path tmp) throws
Exception {
+ Path fake = writeFakeCamel(tmp.resolve("camel"), VERSION);
+ String script = ". " + LIB.toAbsolutePath()
+ + "; assert_camel_cli '" + fake + "' '" + tmp + "' '"
+ VERSION + "'"
+ + " && assert_uninstalled '" +
tmp.resolve("does-not-exist-camel").toAbsolutePath() + "'";
+ Result r = sh(null, List.of("ASSERT_INIT_FIXTURE=" +
FIXTURE.toAbsolutePath()), script);
+
+ assertThat(r.exit()).as(r.out()).isZero();
+ // Assert the specific behavioral lines, not just "PASS": a passing
version comparison and a
+ // fixture-matching init route are what this test exists to protect.
+ assertThat(r.out())
+ .contains("camel version matches expected '" + VERSION + "'")
+ .contains("camel init route content matches expected fixture")
+ .doesNotContain("FAIL");
+ }
+
+ @Test
+ void assertionLibraryFailsWhenInitContentMissing(@TempDir Path tmp) throws
Exception {
+ // Version reports correctly (so the version check passes); only the
init route content is wrong,
+ // so the failure this test asserts can only come from the
init-content comparison.
+ Path fake = writeExecutable(tmp.resolve("camel"),
+ "#!/bin/sh\n"
+ + "case \"$1\" in\n"
+ + " --version) echo
'Apache Camel " + VERSION + "' ;;\n"
+ + " init) echo
'nope' > \"$2\" ;;\n"
+ + " *) exit 3 ;;\n"
+ + "esac\n");
+ String script = ". " + LIB.toAbsolutePath() + "; assert_camel_cli '" +
fake + "' '" + tmp + "' '" + VERSION + "'";
+ Result r = sh(null, List.of("ASSERT_INIT_FIXTURE=" +
FIXTURE.toAbsolutePath()), script);
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out())
+ .contains("camel init route content differs from fixture")
+ .doesNotContain("camel init route content matches expected
fixture");
+ }
+
+ @Test
+ void assertionLibraryFailsOnVersionMismatch(@TempDir Path tmp) throws
Exception {
+ // Init content is correct, but the reported version is wrong. This
pins the fix that makes a
+ // version mismatch propagate out of assert_camel_cli instead of being
printed and swallowed.
+ Path fake = writeFakeCamel(tmp.resolve("camel"), "9.9.9");
+ String script = ". " + LIB.toAbsolutePath() + "; assert_camel_cli '" +
fake + "' '" + tmp + "' '" + VERSION + "'";
+ Result r = sh(null, List.of("ASSERT_INIT_FIXTURE=" +
FIXTURE.toAbsolutePath()), script);
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out()).contains("camel version mismatch: expected '" +
VERSION + "', got '9.9.9'");
+ }
+
+ @Test
+ void unknownTargetIsUsageError() throws Exception {
+ // The script's contract is to reject an unknown command with exit
code 2; assert the exit code
+ // directly rather than only grepping output (usage text prints for
valid commands too).
+ Result r = sh(null, List.of("PATH=/usr/bin:/bin"), "/bin/sh " +
VALIDATE.toAbsolutePath() + " frobnicate");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("Error:").contains("unknown command");
+ }
+
+ @Test
+ void allValidatorsSelfSkipWhenNothingIsStaged(@TempDir Path emptyTarget)
throws Exception {
+ // With an empty target dir, every validator has nothing to act on:
local finds no archive,
+ // homebrew finds no formula, SDKMAN finds no descriptor. All must
SKIP, none may FAIL, exit 0.
+ Result r = sh(null, List.of(
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + emptyTarget.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " all");
+
+ assertThat(r.exit()).as(r.out()).isZero();
+ assertThat(r.out()).contains("SKIP:").doesNotContain("FAIL");
+ }
+
+ @Test
+ void localArchiveValidatorPassesForStagedArchive(@TempDir Path target)
throws Exception {
+ // Stage a real tar.gz laid out exactly like the release archive
(camel-launcher-<v>/bin/camel.sh)
+ // and run the `local` validator against it. This is the one validator
the whole dispatcher design
+ // is built around, so it gets a genuine end-to-end happy-path
assertion, not just an exit code.
+ stageLocalArchive(target);
+ Result r = sh(null, List.of(
+ "ASSERT_INIT_FIXTURE=" + FIXTURE.toAbsolutePath(),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " local");
+
+ assertThat(r.exit()).as(r.out()).isZero();
+ assertThat(r.out())
+ .contains("PASS: local archive version OK")
+ .contains("camel init route content matches expected fixture")
+ .doesNotContain("FAIL");
+ }
+
+ @Test
+ void localArchiveValidatorAcceptsSnapshotVersionInTestMode(@TempDir Path
target) throws Exception {
+ // In test mode the staged archive can be a byte-for-byte copy of a
real SNAPSHOT build (see the CI
+ // workflow's "stage a synthetic release version" step, which cp's the
archive rather than rebuilding
+ // it), so its embedded catalog version genuinely reports
"<VERSION>-SNAPSHOT", not "<VERSION>". Pins
+ // the tolerant comparison branch that accepts this instead of failing
it as a version mismatch.
+ stageLocalArchive(target, VERSION + "-SNAPSHOT");
+ Result r = sh(null, List.of(
+ "ASSERT_INIT_FIXTURE=" + FIXTURE.toAbsolutePath(),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " local");
+
+ assertThat(r.exit()).as(r.out()).isZero();
+ assertThat(r.out())
+ .contains("PASS: local archive version OK (test-mode staged
archive reports its real build "
+ + "version '" + VERSION + "-SNAPSHOT')")
+ .doesNotContain("FAIL");
+ }
+
+ @Test
+ void localArchiveValidatorSurfacesStderrOnEmptyVersionOutput(@TempDir Path
target) throws Exception {
+ // The hardening pass added stderr capture to every
FAIL-on-empty-version-output branch so the failure
+ // message carries a reason instead of just "returned empty output".
Pin that the "(stderr: ...)"
+ // suffix actually appears for the local-archive validator's own FAIL
branch.
+ stageLocalArchiveWithCamelShBody(target, "#!/bin/sh\n"
+ + "case \"$1\" in\n"
+ + " --version) echo 'boom:
cannot determine version' >&2; exit 1 ;;\n"
+ + " *) exit 3 ;;\n"
+ + "esac\n");
+
+ Result r = sh(null, List.of(
+ "ASSERT_INIT_FIXTURE=" + FIXTURE.toAbsolutePath(),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " local");
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out())
+ .contains("FAIL: 'camel.sh --version' returned empty output")
+ .contains("(stderr: boom: cannot determine version)");
+ }
+
+ @Test
+ void versionOverrideRequiresTestMode() throws Exception {
+ // CAMEL_PACKAGE_TEST_VERSION without CAMEL_PACKAGE_TEST_MODE=true
must be rejected (exit 2), so
+ // production can never silently run against a synthetic version.
+ Result r = sh(null, List.of("CAMEL_PACKAGE_TEST_VERSION=" + VERSION),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " local");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("CAMEL_PACKAGE_TEST_VERSION requires
CAMEL_PACKAGE_TEST_MODE=true");
+ }
+
+ @Test
+ void unknownOptionIsUsageError() throws Exception {
+ Result r = sh(null, List.of("PATH=/usr/bin:/bin"), "/bin/sh " +
VALIDATE.toAbsolutePath() + " all --bogus");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("unknown option");
+ }
+
+ @Test
+ void helpIsUsageError() throws Exception {
+ Result r = sh(null, List.of("PATH=/usr/bin:/bin"), "/bin/sh " +
VALIDATE.toAbsolutePath() + " help");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("Usage:");
+ }
+
+ @Test
+ void invalidChannelIsUsageError() throws Exception {
+ Result r = sh(null, List.of("PATH=/usr/bin:/bin"),
+ "/bin/sh " + VALIDATE.toAbsolutePath() + " all --channel
bogus");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("Error:").contains("--channel must be
'stable' or 'lts'");
+ }
+
+ @Test
+ void ltsChannelWithoutLtsLineIsUsageError() throws Exception {
+ Result r = sh(null, List.of("PATH=/usr/bin:/bin"),
+ "/bin/sh " + VALIDATE.toAbsolutePath() + " all --channel lts");
+
+ assertThat(r.exit()).as(r.out()).isEqualTo(2);
+ assertThat(r.out()).contains("Error:").contains("--channel lts
requires --lts-line X.Y");
+ }
+
+ @Test
+ void ltsChannelWithLtsLineIsAccepted(@TempDir Path emptyTarget) throws
Exception {
+ // With --lts-line supplied, --channel lts must pass argument
validation and proceed to the
+ // validators themselves (which SKIP here since nothing is staged),
rather than hitting the usage
+ // error pinned by this test's two siblings above.
+ Result r = sh(null, List.of(
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + emptyTarget.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " all --channel lts
--lts-line 4.21");
+
+ assertThat(r.exit()).as(r.out()).isZero();
+
assertThat(r.out()).contains("SKIP:").doesNotContain("FAIL").doesNotContain("Error:");
+ }
+
+ @Test
+ void dispatcherPropagatesValidatorFailure(@TempDir Path target) throws
Exception {
+ // A validator that FAILs (here the local archive reports the wrong
version) must make the
+ // dispatcher itself exit nonzero - the FAIL must not be swallowed by
main's rc aggregation.
+ stageLocalArchive(target, "9.9.9");
+ Result r = sh(null, List.of(
+ "ASSERT_INIT_FIXTURE=" + FIXTURE.toAbsolutePath(),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " local");
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out()).contains("camel version
mismatch").contains("FAIL");
+ }
+
+ @Test
+ void allDispatcherAggregatesAFailureAcrossValidators(@TempDir Path target)
throws Exception {
+ // Unlike dispatcherPropagatesValidatorFailure above (which calls the
`local` validator
+ // directly), this drives main()'s actual `all` dispatch:
validate_local_archive || rc=1;
+ // validate_homebrew || rc=1; validate_sdkman || rc=1. Nothing is
staged for homebrew/SDKMAN,
+ // so they SKIP - proving the local FAIL doesn't short-circuit the
remaining legs and still
+ // survives to the aggregated exit code.
+ stageLocalArchive(target, "9.9.9");
+ Result r = sh(null, List.of(
+ "ASSERT_INIT_FIXTURE=" + FIXTURE.toAbsolutePath(),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath()),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " all");
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out())
+ .contains("camel version mismatch")
+ .contains("FAIL")
+ // Which SKIP reason fires depends on whether `brew` is on
PATH for the host running this
+ // test: package-native-validation.yml's
ubuntu-latest/macos-latest legs put Linuxbrew/
+ // Homebrew on PATH first, so validate_homebrew() gets past
the host-gate and SKIPs on the
+ // formula file (never staged here). The generic "Build and
test" workflow runs this same
+ // test via a plain reactor `mvn test` with no such PATH
setup, so it SKIPs at the host-gate
+ // instead. Either way the local FAIL didn't short-circuit the
homebrew leg, which is what
+ // this test is actually proving.
+ .containsPattern("SKIP: homebrew (not available|formula not
found)")
+ // `sdk` is a shell function sourced from sdkman-init.sh, not
a PATH binary, so a bare
+ // subprocess (this test's `sh`, and CI's separate "Run POSIX
validator unit tests" step)
+ // always hits the host-gate SKIP rather than the
descriptor-not-found one.
+ .contains("SKIP: sdkman not available");
+ }
+
+ @Test
+ void assertUninstalledFlagsLeftoverButPassesWhenAbsent(@TempDir Path tmp)
throws Exception {
+ // Exercise the multi-arg assert_uninstalled helper directly:
all-absent passes, a leftover fails.
+ Path present = Files.writeString(tmp.resolve("leftover"), "x");
+ Path absent = tmp.resolve("gone");
+
+ Result ok = sh(null, List.of(), ". " + LIB.toAbsolutePath() + ";
assert_uninstalled '" + absent + "'");
+ assertThat(ok.exit()).as(ok.out()).isZero();
+ assertThat(ok.out()).contains("does not exist");
+
+ Result bad = sh(null, List.of(), ". " + LIB.toAbsolutePath() + ";
assert_uninstalled '" + present + "'");
+ assertThat(bad.exit()).as(bad.out()).isNotZero();
+ assertThat(bad.out()).contains("uninstall left behind");
+ }
+
+ @Test
+ void homebrewValidatorFailsWhenAuditStrictReportsIssues(@TempDir Path tmp)
throws Exception {
+ // Pin that a nonzero `brew audit --strict` exit always propagates as
a FAIL and a nonzero
+ // overall exit, never a warning.
+ Path fakeBinDir = Files.createDirectory(tmp.resolve("fake-bin"));
+ writeFakeBrew(fakeBinDir.resolve("brew"));
+ Path target = Files.createDirectory(tmp.resolve("target"));
+ Path tapDir = Files.createDirectory(tmp.resolve("tap"));
+ stageHomebrewFormula(target, "camel-launcher-9.9.9-bin.tar.gz");
+
+ Result r = sh(null, List.of(
+ "PATH=" + fakeBinDir.toAbsolutePath() + File.pathSeparator +
System.getenv("PATH"),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath(),
+ "FAKE_BREW_TAP_DIR=" + tapDir.toAbsolutePath(),
+ "FAKE_BREW_AUDIT_RC=1",
+ "FAKE_BREW_AUDIT_OUTPUT=Error: FormulaAudit/Foo: fake audit
issue"),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " homebrew");
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out()).contains("FAIL: homebrew audit --strict reported
issues (exit 1)");
+ }
+
+ @Test
+ void homebrewValidatorSkipsWhenJavaDependencyMissing(@TempDir Path tmp)
throws Exception {
+ // A `brew install` failure caused by a missing/incompatible Java on
the host is an environment
+ // limitation, not a defect in the generated formula, and must SKIP
(exit 0), not FAIL - this pins
+ // the classification heuristic so a real regression can't get
silently downgraded the same way.
+ Path fakeBinDir = Files.createDirectory(tmp.resolve("fake-bin"));
+ writeFakeBrew(fakeBinDir.resolve("brew"));
+ Path target = Files.createDirectory(tmp.resolve("target"));
+ Path tapDir = Files.createDirectory(tmp.resolve("tap"));
+ stageHomebrewFormula(target, "camel-launcher-9.9.9-bin.tar.gz");
+
+ Result r = sh(null, List.of(
+ "PATH=" + fakeBinDir.toAbsolutePath() + File.pathSeparator +
System.getenv("PATH"),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath(),
+ "FAKE_BREW_TAP_DIR=" + tapDir.toAbsolutePath(),
+ "FAKE_BREW_INSTALL_RC=1",
+ "FAKE_BREW_INSTALL_OUTPUT=Error: no suitable java version
found, please install java"),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " homebrew");
+
+ assertThat(r.exit()).as(r.out()).isZero();
+ assertThat(r.out())
+ .contains("SKIP: homebrew install could not resolve a Java
dependency")
+ .doesNotContain("FAIL");
+ }
+
+ @Test
+ void homebrewAuditFailureSurvivesASubsequentInstallSkip(@TempDir Path tmp)
throws Exception {
+ // A failed `brew audit --strict` is a real formula defect (see
+ // homebrewValidatorFailsWhenAuditStrictReportsIssues above). If
install *also* hits an
+ // unrelated environment limitation (missing Java) afterwards, that
SKIP must not erase the
+ // already-recorded audit failure - the two conditions are independent
and both can be true at
+ // once. Pins the fix for validate_homebrew's SKIP branches returning
the accumulated $rc
+ // instead of an unconditional 0.
+ Path fakeBinDir = Files.createDirectory(tmp.resolve("fake-bin"));
+ writeFakeBrew(fakeBinDir.resolve("brew"));
+ Path target = Files.createDirectory(tmp.resolve("target"));
+ Path tapDir = Files.createDirectory(tmp.resolve("tap"));
+ stageHomebrewFormula(target, "camel-launcher-9.9.9-bin.tar.gz");
+
+ Result r = sh(null, List.of(
+ "PATH=" + fakeBinDir.toAbsolutePath() + File.pathSeparator +
System.getenv("PATH"),
+ "CAMEL_PACKAGE_TEST_MODE=true",
+ "CAMEL_PACKAGE_TEST_VERSION=" + VERSION,
+ "CAMEL_VALIDATE_TARGET_DIR=" + target.toAbsolutePath(),
+ "FAKE_BREW_TAP_DIR=" + tapDir.toAbsolutePath(),
+ "FAKE_BREW_AUDIT_RC=1",
+ "FAKE_BREW_AUDIT_OUTPUT=Error: FormulaAudit/Foo: fake audit
issue",
+ "FAKE_BREW_INSTALL_RC=1",
+ "FAKE_BREW_INSTALL_OUTPUT=Error: no suitable java version
found, please install java"),
+ "/bin/bash " + VALIDATE.toAbsolutePath() + " homebrew");
+
+ assertThat(r.exit()).as(r.out()).isNotZero();
+ assertThat(r.out())
+ .contains("FAIL: homebrew audit --strict reported issues (exit
1)")
+ .contains("SKIP: homebrew install could not resolve a Java
dependency");
+ }
+
+ // Note: the sibling "Failed to download resource|curl: (" SKIP branch
(camel-validate.sh, same
+ // return "$rc" fix as above) is not covered by an equivalent test here.
It only triggers when
+ // CAMEL_PACKAGE_TEST_MODE is NOT "true", but CAMEL_VALIDATE_TARGET_DIR
(needed to point the
+ // validator at an isolated @TempDir instead of this module's real
target/) requires
+ // CAMEL_PACKAGE_TEST_MODE=true - the two guards are mutually exclusive,
so this branch cannot be
+ // reached hermetically the way every other test in this file is. It
shares the exact same
+ // `return "$rc"` line as the covered branch above, so the risk left
uncovered is narrow: whether
+ // the "Failed to download resource" text is classified correctly, not the
rc-propagation logic.
+
+ // ---------- helpers ----------
+
+ /** Body of a conformant fake `camel` CLI: reports {@code version} and
generates the fixture route on init. */
+ private static String fakeCamelBody(String version) {
+ return "#!/bin/sh\n"
+ + "case \"$1\" in\n"
+ + " --version) echo 'Apache Camel " + version + "'; exit 0
;;\n"
+ + " init) name=$(basename \"$2\" .java); "
+ + "sed \"s/public class hello/public class $name/\" '" +
FIXTURE.toAbsolutePath()
+ + "' > \"$2\"; exit 0 ;;\n"
+ + " *) exit 3 ;;\n"
+ + "esac\n";
+ }
+
+ private static Path writeFakeCamel(Path file, String version) throws
IOException {
+ return writeExecutable(file, fakeCamelBody(version));
+ }
+
+ /**
+ * A fake {@code brew} that stands in for Homebrew in {@code
validate_homebrew} so its SKIP-vs-FAIL classification
+ * heuristics can be pinned without needing a real Homebrew install.
Behavior for the {@code audit} and
+ * {@code install} subcommands is driven by {@code
FAKE_BREW_AUDIT_RC}/{@code
+ * FAKE_BREW_AUDIT_OUTPUT} and {@code FAKE_BREW_INSTALL_RC}/{@code
FAKE_BREW_INSTALL_OUTPUT}; the tap directory it
+ * reports is {@code FAKE_BREW_TAP_DIR}. Every other subcommand it needs
(tap-new, untap, style, uninstall) just
+ * succeeds.
+ */
+ private static Path writeFakeBrew(Path file) throws IOException {
+ return writeExecutable(file, "#!/bin/sh\n"
+ + "case \"$1\" in\n"
+ + " tap-new) mkdir -p
\"$FAKE_BREW_TAP_DIR/Formula\"; exit 0 ;;\n"
+ + " --repository) echo
\"$FAKE_BREW_TAP_DIR\"; exit 0 ;;\n"
+ + " untap) exit 0 ;;\n"
+ + " style) exit 0 ;;\n"
+ + " audit)\n"
+ + " [ -n
\"$FAKE_BREW_AUDIT_OUTPUT\" ] && echo \"$FAKE_BREW_AUDIT_OUTPUT\"\n"
+ + " exit
\"${FAKE_BREW_AUDIT_RC:-0}\" ;;\n"
+ + " install)\n"
+ + " [ -n
\"$FAKE_BREW_INSTALL_OUTPUT\" ] && echo \"$FAKE_BREW_INSTALL_OUTPUT\"\n"
+ + " exit
\"${FAKE_BREW_INSTALL_RC:-0}\" ;;\n"
+ + " uninstall) exit 0 ;;\n"
+ + " *) exit 0 ;;\n"
+ + "esac\n");
+ }
+
+ /**
+ * Stages {@code
<targetDir>/jreleaser/package/camel-cli/brew/Formula/apache-camel.rb} (the path
+ * {@code validate_homebrew} reads for the default "stable" channel) plus
a same-named dummy archive under
+ * {@code targetDir}, so the script's real {@code sed}-based url/sha256
rewrite for the offline install has
+ * something to operate on.
+ */
+ private static void stageHomebrewFormula(Path targetDir, String
archiveBasename) throws Exception {
+ Path formulaDir =
Files.createDirectories(targetDir.resolve("jreleaser/package/camel-cli/brew/Formula"));
+ Files.writeString(targetDir.resolve(archiveBasename), "dummy archive
content");
+ String formula = "class ApacheCamel < Formula\n"
+ + " url
\"https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/9.9.9/"
+ + archiveBasename + "\"\n"
+ + " sha256 \"" + "0".repeat(64) + "\"\n"
+ + " version \"9.9.9\"\n"
+ + "end\n";
+ Files.writeString(formulaDir.resolve("apache-camel.rb"), formula);
+ }
+
+ private static Path writeExecutable(Path file, String body) throws
IOException {
+ Files.writeString(file, body, StandardCharsets.UTF_8);
+ Files.setPosixFilePermissions(file, Set.of(
+ PosixFilePermission.OWNER_READ,
+ PosixFilePermission.OWNER_WRITE,
+ PosixFilePermission.OWNER_EXECUTE));
+ return file;
+ }
+
+ private static void stageLocalArchive(Path targetDir) throws Exception {
+ stageLocalArchive(targetDir, PackageNativeValidationTest.VERSION);
+ }
+
+ /**
+ * Builds {@code <targetDir>/camel-launcher-<VERSION>-bin.tar.gz} whose
bin/camel.sh reports {@code reportedVersion}
+ * (pass a value other than VERSION to drive a version-mismatch FAIL).
+ */
+ private static void stageLocalArchive(Path targetDir, String
reportedVersion) throws Exception {
+ stageLocalArchiveWithCamelShBody(targetDir,
fakeCamelBody(reportedVersion));
+ }
+
+ /** Same staging as {@link #stageLocalArchive(Path, String)}, but with an
arbitrary bin/camel.sh body. */
+ private static void stageLocalArchiveWithCamelShBody(Path targetDir,
String camelShBody) throws Exception {
+ Path work = Files.createTempDirectory("camel-archive");
+ String root = "camel-launcher-" + PackageNativeValidationTest.VERSION;
+ Path bin = work.resolve(root).resolve("bin");
+ Files.createDirectories(bin);
+ writeExecutable(bin.resolve("camel.sh"), camelShBody);
+
+ Files.createDirectories(targetDir);
+ Path archive = targetDir.resolve(root + "-bin.tar.gz");
+ Result r = sh(work, List.of(), "tar czf '" + archive + "' '" + root +
"'");
+ assertThat(r.exit()).as("staging archive failed: " + r.out()).isZero();
+ }
+
+ private static Result sh(Path cwd, List<String> envOverrides, String...
commands) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", String.join("
&& ", commands));
+ if (cwd != null) {
+ pb.directory(cwd.toFile());
+ }
+ for (String e : envOverrides) {
+ int eq = e.indexOf('=');
+ pb.environment().put(e.substring(0, eq), e.substring(eq + 1));
+ }
+
+ pb.redirectErrorStream(true);
+ Process p = pb.start();
+ String out = new String(p.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
+ boolean completed = p.waitFor(120, TimeUnit.SECONDS);
+ if (!completed) {
+ p.destroyForcibly();
+ }
+ int exitCode = completed ? p.exitValue() : -1;
+
+ return new Result(exitCode, out);
+ }
+
+ record Result(int exit, String out) {
+ }
+}
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
index 8f62ba6bdb8e..0fbda6496e9d 100644
---
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
@@ -699,6 +699,17 @@ class WebsiteInstallTest {
.resolve(version);
}
+ // Runs the installed 'camel' shim by name, resolved through
PATH+PATHEXT with binDir prepended,
+ // the way an end user invokes it after install. See
forwardsArgumentsAndExitCodeThroughShim for
+ // why a bare command name is used rather than the shim's absolute
path.
+ private static ProcessBuilder shimProcess(Path binDir, String... args)
{
+ List<String> command = new ArrayList<>(List.of("cmd.exe", "/c",
"camel"));
+ command.addAll(Arrays.asList(args));
+ ProcessBuilder pb = new
ProcessBuilder(command).redirectErrorStream(true);
+ pb.environment().put("PATH", binDir + File.pathSeparator +
System.getenv("PATH"));
+ return pb;
+ }
+
private static void assertVersionInstalled(Path home, String version)
throws Exception {
Path shim = expectedBinDir(home).resolve("camel.cmd");
assertTrue(Files.isRegularFile(shim), "expected shim at " + shim);
@@ -1020,16 +1031,20 @@ class WebsiteInstallTest {
publishLatest(fixture, "1.0.0");
assertEquals(0, install(fixture, home, null).exit());
- Path shim = expectedBinDir(home).resolve("camel.cmd");
+ Path binDir = expectedBinDir(home);
+ assertTrue(Files.isRegularFile(binDir.resolve("camel.cmd")),
"expected shim in " + binDir);
- Process argsProc = new ProcessBuilder("cmd.exe", "/c",
shim.toString(), "echo-args", "foo", "bar baz")
- .redirectErrorStream(true).start();
+ // Invoke the shim by name via PATH, exactly as an end user
would after install adds bin to
+ // PATH. Passing the shim's absolute path to `cmd /c` together
with a second quoted argument
+ // ("bar baz") trips cmd.exe's quote-stripping rule (it only
preserves quotes when there are
+ // exactly two), which would unquote the space in "Apache
Camel" and break the call. A bare
+ // command name resolved through PATH+PATHEXT avoids that
quirk and mirrors real usage.
+ Process argsProc = shimProcess(binDir, "echo-args", "foo",
"bar baz").start();
String argsOut = new
String(argsProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
argsProc.waitFor(30, TimeUnit.SECONDS);
assertTrue(argsOut.contains("foo") && argsOut.contains("bar
baz"), argsOut);
- Process exitProc = new ProcessBuilder("cmd.exe", "/c",
shim.toString(), "exit-code", "7")
- .redirectErrorStream(true).start();
+ Process exitProc = shimProcess(binDir, "exit-code",
"7").start();
exitProc.waitFor(30, TimeUnit.SECONDS);
assertEquals(7, exitProc.exitValue());
}
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
index dce2789cbda2..a94857d9b708 100644
---
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
@@ -96,29 +96,59 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
static WebsiteInstallerFixture start(Path temp) throws Exception {
Files.createDirectories(temp);
- Path keystore = temp.resolve("installer-test.p12");
- Path caCertPem = temp.resolve("installer-test-ca.pem");
- generateSelfSignedKeystore(keystore, caCertPem);
-
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- try (var in = Files.newInputStream(keystore)) {
- keyStore.load(in, KEYSTORE_PASSWORD.toCharArray());
- }
- KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray());
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+ SharedTls tls = sharedTls();
HttpsServer server = HttpsServer.create(new
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
- server.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ server.setHttpsConfigurator(new HttpsConfigurator(tls.sslContext()));
- WebsiteInstallerFixture fixture = new WebsiteInstallerFixture(server,
temp, caCertPem);
+ WebsiteInstallerFixture fixture = new WebsiteInstallerFixture(server,
temp, tls.caCertPem());
server.createContext("/", fixture::handle);
server.setExecutor(Executors.newCachedThreadPool());
server.start();
return fixture;
}
+ /** Loopback TLS material (server key pair + exported CA) shared by every
fixture instance in the JVM. */
+ private record SharedTls(SSLContext sslContext, Path caCertPem) {
+ }
+
+ private static volatile SharedTls sharedTls;
+
+ // Minting the self-signed cert costs two keytool.exe launches (each a
full JVM cold start on Windows),
+ // which dominated per-test wall time when done in every start(). The cert
is not test-specific
+ // (CN=127.0.0.1), so generate it once and reuse it - every fixture still
binds its own ephemeral port
+ // and serves its own content. Double-checked locking keeps the one-time
generation thread-safe.
+ private static SharedTls sharedTls() throws Exception {
+ SharedTls local = sharedTls;
+ if (local != null) {
+ return local;
+ }
+ synchronized (WebsiteInstallerFixture.class) {
+ if (sharedTls != null) {
+ return sharedTls;
+ }
+ Path dir = Files.createTempDirectory("camel-installer-tls");
+ dir.toFile().deleteOnExit();
+ Path keystore = dir.resolve("installer-test.p12");
+ Path caCertPem = dir.resolve("installer-test-ca.pem");
+ keystore.toFile().deleteOnExit();
+ caCertPem.toFile().deleteOnExit();
+ generateSelfSignedKeystore(keystore, caCertPem);
+
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ try (var in = Files.newInputStream(keystore)) {
+ keyStore.load(in, KEYSTORE_PASSWORD.toCharArray());
+ }
+ KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray());
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+
+ sharedTls = new SharedTls(sslContext, caCertPem);
+ return sharedTls;
+ }
+ }
+
private static void generateSelfSignedKeystore(Path keystore, Path
caCertPem) throws Exception {
runTool(new ProcessBuilder(
keytool(),
@@ -126,13 +156,21 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
"-alias", KEY_ALIAS,
"-keyalg", "RSA",
"-keysize", "2048",
- "-validity", "2",
+ // Long validity because a single shared cert now backs every
fixture for the whole JVM
+ // lifetime, which under a reused mvnd daemon can span days.
Loopback-only, never trusted
+ // system-wide, so a wide window is harmless.
+ "-validity", "3650",
"-keystore", keystore.toString(),
"-storetype", "PKCS12",
"-storepass", KEYSTORE_PASSWORD,
"-keypass", KEYSTORE_PASSWORD,
"-dname", "CN=127.0.0.1",
- "-ext", "san=ip:127.0.0.1"));
+ "-ext", "san=ip:127.0.0.1",
+ // install.ps1's Windows PowerShell 5.1 path builds a .NET
X509Chain against this
+ // exported cert (see install.ps1's
ServerCertificateValidationCallback); X509Chain.Build
+ // only accepts a chain anchor whose Basic Constraints mark it
as a CA, which keytool does
+ // not set by default.
+ "-ext", "bc:c"));
runTool(new ProcessBuilder(
keytool(),