diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md
index 4672dcd5a9c3c..7c9b3d23d5ba4 100644
--- a/.github/CI-ARCHITECTURE.md
+++ b/.github/CI-ARCHITECTURE.md
@@ -154,9 +154,27 @@ Both methods run in parallel. Results are merged (union) before testing. This le
2. **No regression** — If Scalpel fails, grep results are still used
3. **Gradual migration** — Once Scalpel is validated, grep can be removed
-Scalpel is configured permanently in `.mvn/extensions.xml` (version `0.1.0`). On developer machines it is a no-op — without CI environment variables (`GITHUB_BASE_REF`), no base branch is detected and Scalpel returns immediately. The `mvn validate` with report mode adds ~60-90 seconds in CI.
+Scalpel is configured permanently in `.mvn/extensions.xml`. On developer machines it is a no-op (disabled via `-Dscalpel.enabled=false` in `.mvn/maven.config`). The CI script overrides this with `-Dscalpel.enabled=true`. The `mvn validate` with report mode adds ~60-90 seconds in CI.
-Note: the script overrides `fullBuildTriggers` to empty (`-Dscalpel.fullBuildTriggers=`) because Scalpel's default (`.mvn/**`) would trigger a full build whenever `.mvn/extensions.xml` itself changes (e.g., Dependabot bumping Scalpel).
+#### Scalpel features used for shadow comparison
+
+- **Source-set-aware propagation**: Distinguishes test-jar dependencies from regular dependencies. A module that depends only on another module's test-jar (e.g., `camel-core`'s test-jar with test utilities) is propagated through the `TEST` source set, not the `MAIN` source set. This prevents a change to test utilities from triggering tests in all ~500 modules that depend on `camel-core`.
+- **`skipTestsForDownstreamModules`**: Allows specifying modules whose tests should be skipped when they appear as downstream dependents (mirrors the `EXCLUSION_LIST` in `incremental-build.sh`). This gives Scalpel an accurate picture of what skip-tests mode would actually test.
+
+#### Shadow comparison
+
+Scalpel runs in **shadow mode**: it observes what skip-tests mode *would* have done and reports it in a collapsible section of the PR comment, without affecting actual test execution. This allows the team to validate Scalpel's decisions across many PRs before switching to Scalpel-driven test execution.
+
+The shadow comparison section shows:
+- How many modules Scalpel would test (direct + downstream)
+- How many downstream modules would have tests skipped (generated code, meta-modules)
+- The full list of modules in each category
+
+#### Configuration notes
+
+The script overrides `fullBuildTriggers` to empty (`-Dscalpel.fullBuildTriggers=`) because Scalpel's default (`.mvn/**`) would trigger a full build whenever `.mvn/extensions.xml` itself changes (e.g., Dependabot bumping Scalpel).
+
+The base branch is pre-fetched by the CI workflow (`git fetch --deepen=200` + fetch of `origin/main`). Both the grep-based script and Scalpel use this local git history to compute the merge-base and derive the changed-file diff — no GitHub API call is needed for diff fetching. Scalpel disables its built-in JGit fetch (`-Dscalpel.fetchBaseBranch=false`) to avoid JGit issues in shallow CI clones. The `--deepen=200` fetches only commit metadata (not file blobs), adding ~2-3 seconds to the job.
## Manual Integration Test Advisories
diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh
index f0bce642149f5..2b1c8b3428c93 100755
--- a/.github/actions/incremental-build/incremental-build.sh
+++ b/.github/actions/incremental-build/incremental-build.sh
@@ -106,27 +106,19 @@ hasLabel() {
"https://api.github.com/repos/${repository}/issues/${issueNumber}/labels" | jq -r '.[].name' | { grep -c "$label" || true; }
}
-# Fetch the PR diff from the GitHub API. Returns the full unified diff.
+# Compute the diff between the current HEAD and the base branch using local git.
+# Requires the base branch to be fetched (see pr-build-main.yml "Fetch base branch" step).
fetchDiff() {
- local prId="$1"
- local repository="$2"
+ local base_ref="${GITHUB_BASE_REF:-main}"
+ local merge_base
+ merge_base=$(git merge-base "origin/${base_ref}" HEAD 2>/dev/null) || true
- local diff_output
- diff_output=$(curl -s -w "\n%{http_code}" \
- -H "Authorization: Bearer ${GITHUB_TOKEN}" \
- -H "Accept: application/vnd.github.v3.diff" \
- "https://api.github.com/repos/${repository}/pulls/${prId}")
-
- local http_code
- http_code=$(echo "$diff_output" | tail -n 1)
- local diff_body
- diff_body=$(echo "$diff_output" | sed '$d')
-
- if [[ "$http_code" -lt 200 || "$http_code" -ge 300 || -z "$diff_body" ]]; then
- echo "WARNING: Failed to fetch PR diff (HTTP $http_code). Falling back to full build." >&2
+ if [ -z "$merge_base" ]; then
+ echo "WARNING: Could not find merge-base with origin/${base_ref}. Falling back to full build." >&2
return
fi
- echo "$diff_body"
+
+ git diff "${merge_base}" HEAD 2>/dev/null || true
}
# ── POM dependency analysis (previously detect-dependencies) ───────────
@@ -243,19 +235,17 @@ analyzePomDependencies() {
runScalpelDetection() {
echo " Running Scalpel change detection..."
- # Ensure sufficient git history for JGit merge-base detection
- # (CI uses shallow clones; Scalpel needs to find the merge base)
- git fetch origin main:refs/remotes/origin/main --depth=200 2>/dev/null || true
- git fetch --deepen=200 2>/dev/null || true
-
# Scalpel is permanently configured in .mvn/extensions.xml.
# On developer machines it's a no-op (no GITHUB_BASE_REF → no base branch detected).
+ # Base branch is pre-fetched by the CI workflow (fetchBaseBranch=false).
# Run Maven validate with Scalpel in report mode:
# - mode=report: write JSON report without trimming the reactor
# - fullBuildTriggers="": override .mvn/** default (Scalpel lives in .mvn/extensions.xml)
- # - alsoMake/alsoMakeDependents=false: we only want directly affected modules
- # (our script handles -amd expansion separately)
- local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report -Dscalpel.fullBuildTriggers= -Dscalpel.alsoMake=false -Dscalpel.alsoMakeDependents=false"
+ # - fetchBaseBranch=false: base branch is pre-fetched by the CI workflow
+ # - skipTestsForDownstreamModules: mirrors EXCLUSION_LIST — tells Scalpel which
+ # downstream modules should not run tests in skip-tests mode (for shadow comparison)
+ local skip_downstream="camel-allcomponents,camel-catalog,camel-catalog-console,camel-catalog-lucene,camel-catalog-maven,camel-catalog-suggest,camel-endpointdsl,camel-componentdsl,camel-endpointdsl-support,camel-yaml-dsl,camel-kamelet-main,camel-yaml-dsl-deserializers,camel-yaml-dsl-maven-plugin,camel-jbang-core,camel-jbang-main,camel-jbang-plugin-generate,camel-jbang-plugin-edit,camel-jbang-plugin-kubernetes,camel-jbang-plugin-test,camel-launcher,camel-jbang-it,camel-itest,docs,apache-camel,coverage,dummy-component,camel-csimple-maven-plugin,camel-report-maven-plugin,camel-route-parser"
+ local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report -Dscalpel.fullBuildTriggers= -Dscalpel.fetchBaseBranch=false -Dscalpel.excludePaths=.github/** -Dscalpel.skipTestsForDownstreamModules=${skip_downstream}"
# For workflow_dispatch, GITHUB_BASE_REF may not be set
if [ -z "${GITHUB_BASE_REF:-}" ]; then
scalpel_args="$scalpel_args -Dscalpel.baseBranch=origin/main"
@@ -293,9 +283,24 @@ runScalpelDetection() {
scalpel_managed_deps=$(jq -r '(.changedManagedDependencies // []) | if length > 0 then join(", ") else "" end' "$report" 2>/dev/null || true)
scalpel_managed_plugins=$(jq -r '(.changedManagedPlugins // []) | if length > 0 then join(", ") else "" end' "$report" 2>/dev/null || true)
+ # Scalpel shadow comparison data:
+ # - Modules Scalpel skip-tests mode would test (testsSkipped != true)
+ # - Modules Scalpel would skip (testsSkipped == true, from skipTestsForDownstreamModules)
+ # - Breakdown by category (DIRECT, DOWNSTREAM)
+ scalpel_would_test=$(jq -r '[.affectedModules[] | select(.testsSkipped != true)] | map(.artifactId) | sort | join(",")' "$report" 2>/dev/null || true)
+ scalpel_would_skip=$(jq -r '[.affectedModules[] | select(.testsSkipped == true)] | map(.artifactId) | sort | join(",")' "$report" 2>/dev/null || true)
+ scalpel_direct_count=$(jq '[.affectedModules[] | select(.category == "DIRECT")] | length' "$report" 2>/dev/null || echo "0")
+ scalpel_downstream_tested=$(jq '[.affectedModules[] | select(.category == "DOWNSTREAM" and .testsSkipped != true)] | length' "$report" 2>/dev/null || echo "0")
+ scalpel_downstream_skipped=$(jq '[.affectedModules[] | select(.category == "DOWNSTREAM" and .testsSkipped == true)] | length' "$report" 2>/dev/null || echo "0")
+
local mod_count
mod_count=$(jq '.affectedModules | length' "$report" 2>/dev/null || echo "0")
- echo " Scalpel detected $mod_count affected modules"
+ local test_count=0
+ if [ -n "$scalpel_would_test" ]; then
+ test_count=$(echo "$scalpel_would_test" | tr ',' '\n' | grep -c . || true)
+ fi
+ echo " Scalpel detected $mod_count affected modules ($test_count would be tested)"
+ echo " Direct: $scalpel_direct_count, Downstream tested: $scalpel_downstream_tested, Downstream skipped: $scalpel_downstream_skipped"
if [ -n "$scalpel_props" ]; then
echo " Changed properties: $scalpel_props"
fi
@@ -382,6 +387,68 @@ checkManualItTests() {
fi
}
+# ── Scalpel shadow comparison ──────────────────────────────────────────
+
+# Write Scalpel shadow comparison section to the PR comment.
+# Shows what Scalpel skip-tests mode would have tested vs what the current
+# approach actually tested — observation only, does not affect test execution.
+writeScalpelComparison() {
+ local comment_file="$1"
+
+ # Skip if no Scalpel data
+ if [ -z "$scalpel_would_test" ] && [ -z "$scalpel_would_skip" ]; then
+ return
+ fi
+
+ local scalpel_test_count=0
+ local scalpel_skip_count=0
+ if [ -n "$scalpel_would_test" ]; then
+ scalpel_test_count=$(echo "$scalpel_would_test" | tr ',' '\n' | grep -c . || true)
+ fi
+ if [ -n "$scalpel_would_skip" ]; then
+ scalpel_skip_count=$(echo "$scalpel_would_skip" | tr ',' '\n' | grep -c . || true)
+ fi
+
+ echo "" >> "$comment_file"
+ echo ":microscope: Scalpel shadow comparison (skip-tests mode)
" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo "**Scalpel skip-tests mode would test ${scalpel_test_count} modules** (${scalpel_direct_count} direct + ${scalpel_downstream_tested} downstream)" >> "$comment_file"
+
+ if [ "$scalpel_downstream_skipped" -gt 0 ]; then
+ echo "" >> "$comment_file"
+ echo "${scalpel_downstream_skipped} downstream module(s) would have tests skipped (generated code, meta-modules)" >> "$comment_file"
+ fi
+
+ # Show which modules Scalpel would test
+ if [ -n "$scalpel_would_test" ]; then
+ echo "" >> "$comment_file"
+ echo "Modules Scalpel would test (${scalpel_test_count})
" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo "$scalpel_would_test" | tr ',' '\n' | while read -r m; do
+ [ -n "$m" ] && echo "- \`$m\`" >> "$comment_file"
+ done
+ echo "" >> "$comment_file"
+ echo " " >> "$comment_file"
+ fi
+
+ # Show which modules would have tests skipped
+ if [ -n "$scalpel_would_skip" ]; then
+ echo "" >> "$comment_file"
+ echo "Modules with tests skipped (${scalpel_skip_count})
" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo "$scalpel_would_skip" | tr ',' '\n' | while read -r m; do
+ [ -n "$m" ] && echo "- \`$m\`" >> "$comment_file"
+ done
+ echo "" >> "$comment_file"
+ echo " " >> "$comment_file"
+ fi
+
+ echo "" >> "$comment_file"
+ echo "> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo " " >> "$comment_file"
+}
+
# ── Comment generation ─────────────────────────────────────────────────
writeComment() {
@@ -480,11 +547,11 @@ main() {
fi
fi
- # Fetch the diff (PR diff via API, or git diff for push builds)
+ # Compute the diff using local git history (merge-base for PRs, HEAD~1 for push builds)
local diff_body
if [ -n "$prId" ]; then
- echo "Fetching PR #${prId} diff..."
- diff_body=$(fetchDiff "$prId" "$repository")
+ echo "Computing diff against origin/${GITHUB_BASE_REF:-main}..."
+ diff_body=$(fetchDiff)
else
echo "No PR ID, using git diff HEAD~1..."
diff_body=$(git diff HEAD~1 2>/dev/null || true)
@@ -541,6 +608,12 @@ main() {
scalpel_props=""
scalpel_managed_deps=""
scalpel_managed_plugins=""
+ # Scalpel shadow comparison data
+ scalpel_would_test=""
+ scalpel_would_skip=""
+ scalpel_direct_count="0"
+ scalpel_downstream_tested="0"
+ scalpel_downstream_skipped="0"
# Step 2a: Grep-based detection (existing approach)
if [ -n "$pom_files" ]; then
@@ -760,6 +833,9 @@ main() {
local comment_file="incremental-test-comment.md"
writeComment "$comment_file" "$pl" "$dep_module_ids" "$all_changed_props" "$testedDependents" "$extraModules" "$scalpel_managed_deps" "$scalpel_managed_plugins"
+ # Scalpel shadow comparison (observation only)
+ writeScalpelComparison "$comment_file"
+
# Check for tests disabled in CI via @DisabledIfSystemProperty(named = "ci.env.name")
local disabled_tests
disabled_tests=$(detectDisabledTests "$final_pl")
diff --git a/.github/workflows/pr-build-main.yml b/.github/workflows/pr-build-main.yml
index 17e4bf65240ae..fa252e9e0f1c9 100644
--- a/.github/workflows/pr-build-main.yml
+++ b/.github/workflows/pr-build-main.yml
@@ -80,8 +80,14 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- persist-credentials: false
ref: ${{ inputs.pr_ref || '' }}
+ - name: Fetch base branch for Scalpel change detection
+ if: ${{ !inputs.skip_full_build }}
+ run: |
+ # Scalpel needs the merge base between HEAD and the base branch.
+ # The checkout is depth=1, so deepen both sides for merge-base reachability.
+ git fetch --deepen=200 2>/dev/null || true
+ git fetch --no-tags --depth=200 origin "${GITHUB_BASE_REF:-main}:refs/remotes/origin/${GITHUB_BASE_REF:-main}"
- id: install-packages
uses: ./.github/actions/install-packages
- id: install-mvnd
diff --git a/.github/workflows/sonar-build.yml b/.github/workflows/sonar-build.yml
index 1e20713dff161..4182ac121757f 100644
--- a/.github/workflows/sonar-build.yml
+++ b/.github/workflows/sonar-build.yml
@@ -43,9 +43,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
-
+ - name: Fetch base branch for Scalpel change detection
+ run: |
+ git fetch --deepen=200 2>/dev/null || true
+ git fetch --no-tags --depth=200 origin "${GITHUB_BASE_REF:-main}:refs/remotes/origin/${GITHUB_BASE_REF:-main}"
- id: install-packages
uses: ./.github/actions/install-packages
diff --git a/parent/pom.xml b/parent/pom.xml
index 0063fa2bbac87..646c5fc6a4bfc 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -321,7 +321,7 @@
21.0.6
3.2.4
2.4
- 5.13.4
+ 5.14.4
6.0.3
2.3.0
1.0.3