From ae44f531b56c0cc2ca88f5d199bfe9497a3160fc Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 04:42:42 +0000 Subject: [PATCH 1/9] ci: add Scalpel shadow comparison for skip-tests mode validation Add a shadow comparison section to CI PR comments showing what Scalpel's skip-tests mode would have tested, without affecting actual test execution. This validates Scalpel's module detection across many PRs before switching to Scalpel-driven test execution. Changes: - incremental-build.sh: switch from GitHub API diff to local git merge-base, configure Scalpel with skipTestsForDownstreamModules and fetchBaseBranch=false, add writeScalpelComparison() for collapsible PR comment section - pr-build-main.yml / sonar-build.yml: add base branch fetch step for Scalpel's merge-base detection in shallow CI clones - CI-ARCHITECTURE.md: document shadow comparison approach and configuration --- .github/CI-ARCHITECTURE.md | 24 ++- .../incremental-build/incremental-build.sh | 137 ++++++++++++++---- .github/workflows/pr-build-main.yml | 9 +- .github/workflows/sonar-build.yml | 9 +- 4 files changed, 141 insertions(+), 38 deletions(-) diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md index 948f3e11b96a3..0df46f2b642c9 100644 --- a/.github/CI-ARCHITECTURE.md +++ b/.github/CI-ARCHITECTURE.md @@ -154,12 +154,30 @@ 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. - -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 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. Scalpel is only invoked when a **subdirectory** `pom.xml` is changed (e.g. `parent/pom.xml`, `components/camel-kafka/pom.xml`). Changes to the **root** `pom.xml` are excluded because it contains build-infrastructure config (license plugin, checkstyle, etc.) that does not affect module compilation or test behavior. Without this filter, Scalpel would report every module as affected since they all inherit from the root POM. +#### 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 Some modules are excluded from CI's `-amd` expansion (the `EXCLUSION_LIST`) because they are generated code, meta-modules, or expensive integration test suites. When a contributor changes one of these modules, CI cannot automatically test all downstream effects. diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh index 6bd172ef7ae0b..bd3df407d0530 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,18 @@ 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). + # On developer machines it's a no-op (disabled via -Dscalpel.enabled=false in .mvn/maven.config). + # The CI script overrides this with -Dscalpel.enabled=true. + # 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 +284,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 +388,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 +548,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) @@ -539,6 +607,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 @@ -762,6 +836,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 975fa623761e8..b72ac823d4528 100644 --- a/.github/workflows/pr-build-main.yml +++ b/.github/workflows/pr-build-main.yml @@ -76,10 +76,17 @@ jobs: maven_extra_args: '-Denforcer.phase=none' steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - 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 85efb59465492..fdf9d42084b6b 100644 --- a/.github/workflows/sonar-build.yml +++ b/.github/workflows/sonar-build.yml @@ -42,10 +42,11 @@ jobs: name: Build for Sonar Analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 From fd68427d76646d75b7c93c319f5009bbde174c35 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 11:45:54 +0000 Subject: [PATCH 2/9] ci: restore checkout v7 and persist-credentials in CI workflows The checkout action was accidentally downgraded from v7.0.0 to v6.0.2, and persist-credentials: false was dropped from sonar-build.yml. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr-build-main.yml | 2 +- .github/workflows/sonar-build.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-build-main.yml b/.github/workflows/pr-build-main.yml index b72ac823d4528..74cfc82b3863a 100644 --- a/.github/workflows/pr-build-main.yml +++ b/.github/workflows/pr-build-main.yml @@ -76,7 +76,7 @@ jobs: maven_extra_args: '-Denforcer.phase=none' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false ref: ${{ inputs.pr_ref || '' }} diff --git a/.github/workflows/sonar-build.yml b/.github/workflows/sonar-build.yml index fdf9d42084b6b..5dbeb2cdaae52 100644 --- a/.github/workflows/sonar-build.yml +++ b/.github/workflows/sonar-build.yml @@ -42,7 +42,9 @@ jobs: name: Build for Sonar Analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Fetch base branch for Scalpel change detection run: | git fetch --deepen=200 2>/dev/null || true From 7ebbbc57d42f5b8dd611d7e8d7a952f363b059f4 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 12:41:52 +0000 Subject: [PATCH 3/9] ci: keep GitHub API for grep diff, derive skip_downstream from EXCLUSION_LIST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore fetchDiff() to use the GitHub API (unchanged from main) instead of local git merge-base. The grep-based script has no dependency on local git history — only Scalpel needs it. This eliminates regression risk for the existing incremental build path. Also derive skip_downstream from EXCLUSION_LIST via sed instead of hardcoding a duplicate list that could silently diverge. Co-Authored-By: Claude Opus 4.6 --- .github/CI-ARCHITECTURE.md | 2 +- .../incremental-build/incremental-build.sh | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md index 0df46f2b642c9..b407c94dc4333 100644 --- a/.github/CI-ARCHITECTURE.md +++ b/.github/CI-ARCHITECTURE.md @@ -176,7 +176,7 @@ The shadow comparison section shows: 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. +The grep-based script fetches the PR diff via the GitHub REST API (unchanged). Scalpel uses local git history to compare effective POM models — the CI workflow pre-fetches the base branch (`git fetch --deepen=200` + fetch of `origin/main`) so Scalpel's JGit can find the merge-base. 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 bd3df407d0530..715fdd0432fd3 100755 --- a/.github/actions/incremental-build/incremental-build.sh +++ b/.github/actions/incremental-build/incremental-build.sh @@ -106,19 +106,27 @@ hasLabel() { "https://api.github.com/repos/${repository}/issues/${issueNumber}/labels" | jq -r '.[].name' | { grep -c "$label" || true; } } -# 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). +# Fetch the PR diff from the GitHub API. Returns the full unified diff. fetchDiff() { - local base_ref="${GITHUB_BASE_REF:-main}" - local merge_base - merge_base=$(git merge-base "origin/${base_ref}" HEAD 2>/dev/null) || true + local prId="$1" + local repository="$2" - if [ -z "$merge_base" ]; then - echo "WARNING: Could not find merge-base with origin/${base_ref}. Falling back to full build." >&2 + 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 return fi - - git diff "${merge_base}" HEAD 2>/dev/null || true + echo "$diff_body" } # ── POM dependency analysis (previously detect-dependencies) ─────────── @@ -243,9 +251,11 @@ runScalpelDetection() { # - mode=report: write JSON report without trimming the reactor # - fullBuildTriggers="": override .mvn/** default (Scalpel lives in .mvn/extensions.xml) # - fetchBaseBranch=false: base branch is pre-fetched by the CI workflow - # - skipTestsForDownstreamModules: mirrors EXCLUSION_LIST — tells Scalpel which + # - skipTestsForDownstreamModules: derived from 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" + # Strip the Maven "!:" prefix from each entry to get bare artifact IDs for Scalpel. + local skip_downstream + skip_downstream=$(echo "$EXCLUSION_LIST" | sed 's/!://g') 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 @@ -548,11 +558,11 @@ main() { fi fi - # Compute the diff using local git history (merge-base for PRs, HEAD~1 for push builds) + # Fetch the diff (PR diff via API, or git diff for push builds) local diff_body if [ -n "$prId" ]; then - echo "Computing diff against origin/${GITHUB_BASE_REF:-main}..." - diff_body=$(fetchDiff) + echo "Fetching PR #${prId} diff..." + diff_body=$(fetchDiff "$prId" "$repository") else echo "No PR ID, using git diff HEAD~1..." diff_body=$(git diff HEAD~1 2>/dev/null || true) From e9d2a7860946b0a73691d1d373a8ce0e5e0f0dee Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 12:49:59 +0000 Subject: [PATCH 4/9] ci: show Scalpel failure reason in PR comment When Scalpel fails (mvn validate error, missing report due to shallow clone depth, or full-build trigger), surface the reason in the PR comment's shadow comparison section instead of silently omitting it. Co-Authored-By: Claude Opus 4.6 --- .../incremental-build/incremental-build.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh index 715fdd0432fd3..a502bb3d79c1f 100755 --- a/.github/actions/incremental-build/incremental-build.sh +++ b/.github/actions/incremental-build/incremental-build.sh @@ -266,6 +266,7 @@ runScalpelDetection() { ./mvnw -B -q validate $scalpel_args ${MAVEN_EXTRA_ARGS:-} -l /tmp/scalpel-validate.log 2>/dev/null || { echo " WARNING: Scalpel detection failed (exit $?), skipping" grep -i "scalpel" /tmp/scalpel-validate.log 2>/dev/null | head -5 || true + scalpel_failure_reason="Scalpel detection failed (mvn validate exited with error)" return } @@ -274,6 +275,7 @@ runScalpelDetection() { if [ ! -f "$report" ]; then echo " WARNING: Scalpel report not found at $report" grep -i "scalpel" /tmp/scalpel-validate.log 2>/dev/null | head -5 || true + scalpel_failure_reason="Scalpel report not found (merge-base may be unreachable in shallow clone)" return fi @@ -284,6 +286,7 @@ runScalpelDetection() { local trigger_file trigger_file=$(jq -r '.triggerFile // "unknown"' "$report") echo " Scalpel: Full build triggered by change to $trigger_file" + scalpel_failure_reason="Scalpel triggered a full build (changed file: $trigger_file)" return fi @@ -406,7 +409,20 @@ checkManualItTests() { writeScalpelComparison() { local comment_file="$1" - # Skip if no Scalpel data + # If Scalpel failed, show why in the PR comment + if [ -n "$scalpel_failure_reason" ]; then + echo "" >> "$comment_file" + echo "
:microscope: Scalpel shadow comparison (skip-tests mode)" >> "$comment_file" + echo "" >> "$comment_file" + echo ":warning: $scalpel_failure_reason" >> "$comment_file" + 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" + return + fi + + # Skip if no Scalpel data (Scalpel was not invoked for this PR) if [ -z "$scalpel_would_test" ] && [ -z "$scalpel_would_skip" ]; then return fi @@ -623,6 +639,7 @@ main() { scalpel_direct_count="0" scalpel_downstream_tested="0" scalpel_downstream_skipped="0" + scalpel_failure_reason="" # Step 2a: Grep-based detection (existing approach) if [ -n "$pom_files" ]; then From 7da4738d87036fb7f1a19a9e23ef267bebda7642 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 16:23:34 +0000 Subject: [PATCH 5/9] ci: filter Scalpel shadow comparison to DIRECT+DOWNSTREAM modules Scalpel 0.3.5 still includes ~600 uncategorized parent-inherited modules in affectedModules (scalpel#39, regression from #30 fix). Filter jq queries to only DIRECT and DOWNSTREAM categories, and compute the summary count from the breakdown rather than the inflated total. Co-Authored-By: Claude Opus 4.6 --- .../actions/incremental-build/incremental-build.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh index a502bb3d79c1f..3df0b3d492428 100755 --- a/.github/actions/incremental-build/incremental-build.sh +++ b/.github/actions/incremental-build/incremental-build.sh @@ -301,8 +301,10 @@ runScalpelDetection() { # - 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) + # Only count DIRECT and DOWNSTREAM modules — other categories (e.g., parent POM + # children) inflate the total without being meaningfully affected (scalpel#28). + scalpel_would_test=$(jq -r '[.affectedModules[] | select((.category == "DIRECT" or .category == "DOWNSTREAM") and .testsSkipped != true)] | map(.artifactId) | sort | join(",")' "$report" 2>/dev/null || true) + scalpel_would_skip=$(jq -r '[.affectedModules[] | select((.category == "DIRECT" or .category == "DOWNSTREAM") and .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") @@ -439,7 +441,8 @@ writeScalpelComparison() { 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" + local scalpel_targeted_count=$((scalpel_direct_count + scalpel_downstream_tested)) + echo "**Scalpel skip-tests mode would test ${scalpel_targeted_count} modules** (${scalpel_direct_count} direct + ${scalpel_downstream_tested} downstream)" >> "$comment_file" if [ "$scalpel_downstream_skipped" -gt 0 ]; then echo "" >> "$comment_file" @@ -449,7 +452,7 @@ writeScalpelComparison() { # 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 "
Modules Scalpel would test (${scalpel_targeted_count})" >> "$comment_file" echo "" >> "$comment_file" echo "$scalpel_would_test" | tr ',' '\n' | while read -r m; do [ -n "$m" ] && echo "- \`$m\`" >> "$comment_file" From af6275fffb883d0639b97f4261de3b8d8f4c4143 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 25 Jun 2026 17:56:27 +0000 Subject: [PATCH 6/9] Revert "ci: filter Scalpel shadow comparison to DIRECT+DOWNSTREAM modules" This reverts commit 7da4738d87036fb7f1a19a9e23ef267bebda7642. --- .../actions/incremental-build/incremental-build.sh | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh index 3df0b3d492428..a502bb3d79c1f 100755 --- a/.github/actions/incremental-build/incremental-build.sh +++ b/.github/actions/incremental-build/incremental-build.sh @@ -301,10 +301,8 @@ runScalpelDetection() { # - Modules Scalpel skip-tests mode would test (testsSkipped != true) # - Modules Scalpel would skip (testsSkipped == true, from skipTestsForDownstreamModules) # - Breakdown by category (DIRECT, DOWNSTREAM) - # Only count DIRECT and DOWNSTREAM modules — other categories (e.g., parent POM - # children) inflate the total without being meaningfully affected (scalpel#28). - scalpel_would_test=$(jq -r '[.affectedModules[] | select((.category == "DIRECT" or .category == "DOWNSTREAM") and .testsSkipped != true)] | map(.artifactId) | sort | join(",")' "$report" 2>/dev/null || true) - scalpel_would_skip=$(jq -r '[.affectedModules[] | select((.category == "DIRECT" or .category == "DOWNSTREAM") and .testsSkipped == true)] | map(.artifactId) | sort | join(",")' "$report" 2>/dev/null || true) + 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") @@ -441,8 +439,7 @@ writeScalpelComparison() { echo "" >> "$comment_file" echo "
:microscope: Scalpel shadow comparison (skip-tests mode)" >> "$comment_file" echo "" >> "$comment_file" - local scalpel_targeted_count=$((scalpel_direct_count + scalpel_downstream_tested)) - echo "**Scalpel skip-tests mode would test ${scalpel_targeted_count} modules** (${scalpel_direct_count} direct + ${scalpel_downstream_tested} downstream)" >> "$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" @@ -452,7 +449,7 @@ writeScalpelComparison() { # Show which modules Scalpel would test if [ -n "$scalpel_would_test" ]; then echo "" >> "$comment_file" - echo "
Modules Scalpel would test (${scalpel_targeted_count})" >> "$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" From 237419b20e44c7bd56cbb8868420ce43c258601b Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 26 Jun 2026 05:36:09 +0000 Subject: [PATCH 7/9] ci: upgrade Scalpel to 0.3.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes inflated affectedModules count — parent-inherited modules that don't use the changed dependency are no longer included (scalpel#39). Co-Authored-By: Claude Opus 4.6 --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 4a4c3957328d2..33a8c3708582c 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -23,6 +23,6 @@ eu.maveniverse.maven.scalpel extension3 - 0.3.5 + 0.3.6 From d106d6157bb3a3edf20164791463cc2fe7a2a461 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 26 Jun 2026 11:55:37 +0000 Subject: [PATCH 8/9] ci: upgrade Scalpel to 0.3.7 Fixes inflated affectedModules count where parent POM property changes (e.g. kafka-version) were reported as affecting all ~670 modules. Also fixes skipTestsForDownstreamModules not taking effect. See https://github.com/maveniverse/scalpel/issues/39 Co-Authored-By: Claude Opus 4.6 --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 33a8c3708582c..14d5d707fcccc 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -23,6 +23,6 @@ eu.maveniverse.maven.scalpel extension3 - 0.3.6 + 0.3.7 From f418ac88adcce20c4fb3b5bbacfcc49a3ff86664 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 26 Jun 2026 12:29:42 +0000 Subject: [PATCH 9/9] =?UTF-8?q?[DO=20NOT=20MERGE]=20test:=20jackson2-versi?= =?UTF-8?q?on=202.22.0=20=E2=86=92=202.21.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates Scalpel shadow comparison for a widely-used managed dependency change. Excludes .mvn/** from Scalpel diff (this PR inherits .mvn/extensions.xml changes). Co-Authored-By: Claude Opus 4.6 --- .github/actions/incremental-build/incremental-build.sh | 2 +- parent/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/incremental-build/incremental-build.sh b/.github/actions/incremental-build/incremental-build.sh index a502bb3d79c1f..e5543da0b74d2 100755 --- a/.github/actions/incremental-build/incremental-build.sh +++ b/.github/actions/incremental-build/incremental-build.sh @@ -256,7 +256,7 @@ runScalpelDetection() { # Strip the Maven "!:" prefix from each entry to get bare artifact IDs for Scalpel. local skip_downstream skip_downstream=$(echo "$EXCLUSION_LIST" | sed 's/!://g') - local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report -Dscalpel.fullBuildTriggers= -Dscalpel.fetchBaseBranch=false -Dscalpel.excludePaths=.github/** -Dscalpel.skipTestsForDownstreamModules=${skip_downstream}" + local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report -Dscalpel.fullBuildTriggers= -Dscalpel.fetchBaseBranch=false -Dscalpel.excludePaths=.github/**,.mvn/** -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" diff --git a/parent/pom.xml b/parent/pom.xml index 228e7d5994674..1aa3dbea26e24 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -251,7 +251,7 @@ 3.0.5 0.2.9 1.6.2 - 2.22.0 + 2.21.2 2.20 3.2.0 2.22.3