This is an automated email from the ASF dual-hosted git repository.
gnodet 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 64bd35a62a3c ci: improve Scalpel shadow mode using 0.4.x report
features (#26534)
64bd35a62a3c is described below
commit 64bd35a62a3cbbab03bdfaaaf0ef960c643b5e00
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Sep 18 15:58:46 2026 +0200
ci: improve Scalpel shadow mode using 0.4.x report features (#26534)
* ci: improve Scalpel shadow mode using 0.4.x report features
Bump Scalpel extension3 from 0.4.0 to 0.4.2 and leverage new report
fields introduced in the 0.4.x series to improve the shadow comparison
section of PR comments.
Changes:
- Bump Scalpel 0.4.0 → 0.4.2 (.mvn/extensions.xml)
- 0.4.1: fixes buildAllIfNoChanges handling, adds reactorModuleCount,
emits decision inputs, fixes non-reactor file cascade to root module
- 0.4.2: fixes NPE when Scalpel trims to empty (currentProject restore)
- Add -Dscalpel.explain=true to runScalpelDetection()
Each affected module now carries an evidence[] array naming the exact
file, property, dependency, or graph edge that caused it to be included.
The PR comment surfaces this inline per module (e.g.
`camel-kafka` ← properties/version.kafka).
- Use reactorModuleCount report field for N-of-M framing
The summary line now reads "47 of 1,847 tested" instead of a bare count,
without any extra shell arithmetic — the reactor size comes directly from
the report.
- Use testedModulesCount report field directly
Eliminates the manual grep-c counting of scalpel_would_test list.
Falls back to counting affectedModules when the field is absent (older
report schema).
- Move POM change details above the module lists in the PR comment
Changed properties/deps/plugins are more useful as context before the
per-module breakdown.
- Update CI-ARCHITECTURE.md to document the new features
* ci: address review nits
- Pass report path as explicit argument to writeScalpelComparison
instead of re-declaring a relative path inside the function body;
the caller (running from the GHA workspace root) passes the same
path that runScalpelDetection uses, making the contract explicit and
safe if the script is ever invoked from a subdirectory
- Reserve N-of-M suffix for testedModulesCount only; scalpel_total
(affected = tested + compile-only) now shows a plain count so the
two numbers in the PR comment share the same denominator without
confusion about why they differ
---
.github/CI-ARCHITECTURE.md | 8 +-
.../actions/incremental-build/incremental-build.sh | 94 +++++++++++++---------
.mvn/extensions.xml | 2 +-
3 files changed, 63 insertions(+), 41 deletions(-)
diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md
index 9c95094f0f63..d4a16ccfaf3d 100644
--- a/.github/CI-ARCHITECTURE.md
+++ b/.github/CI-ARCHITECTURE.md
@@ -165,16 +165,20 @@ Scalpel is only invoked when a **subdirectory** `pom.xml`
is changed (e.g. `pare
- **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.
+- **Explain mode** (`-Dscalpel.explain=true`): Each affected module carries an
`evidence[]` array naming the exact file, property, managed dependency, or
graph edge that caused it to be included. The PR comment surfaces this evidence
inline — e.g. `` `camel-kafka` ← properties/version.kafka `` — making it
immediately clear why each module is in the build set.
+- **`reactorModuleCount`** (report field, 0.4.1+): Total reactor size emitted
directly by Scalpel. Used for N-of-M framing in the PR comment (e.g. "47 of
1,847 tested") without extra shell arithmetic.
+- **`testedModulesCount`** (report field, 0.4.x): Modules whose tests will
actually run, computed by Scalpel. Used directly instead of counting list
elements in the script.
#### 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 modules Scalpel would test out of the total reactor (N-of-M framing)
- How many downstream modules would have tests skipped (generated code,
meta-modules)
+- POM change details: changed properties, managed dependencies, managed plugins
- Set differences: modules only Scalpel found vs modules only the current
approach found
-- The full list of modules in each category
+- The full list of modules in each category, with per-module evidence (explain
mode)
The comparison is apples-to-apples: the current approach's reactor is filtered
through the `EXCLUSION_LIST` before comparing, so both sides exclude the same
meta/generated modules (catalog, jbang, docs, etc.).
diff --git a/.github/actions/incremental-build/incremental-build.sh
b/.github/actions/incremental-build/incremental-build.sh
index 4a2096d4b7c9..f8e52905e0af 100755
--- a/.github/actions/incremental-build/incremental-build.sh
+++ b/.github/actions/incremental-build/incremental-build.sh
@@ -256,6 +256,7 @@ runScalpelDetection() {
# 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
+ # - explain=true: add per-module evidence[] to the report (exact
file/property/dep that triggered each module)
# - fullBuildTriggers="": override .mvn/** default (Scalpel lives in
.mvn/extensions.xml)
# - fetchBaseBranch=false: base branch is pre-fetched by the CI workflow
# - skipTestsForDownstreamModules: derived from EXCLUSION_LIST — tells
Scalpel which
@@ -266,7 +267,7 @@ runScalpelDetection() {
# Always pass baseBranch explicitly — relying on Scalpel's
env.GITHUB_BASE_REF
# auto-detection is fragile across Maven wrappers and CI rerun contexts.
local base_branch="origin/${GITHUB_BASE_REF:-main}"
- local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report
-Dscalpel.fullBuildTriggers= -Dscalpel.fetchBaseBranch=false
-Dscalpel.baseBranch=${base_branch} -Dscalpel.excludePaths=.github/**
-Dscalpel.skipTestsForDownstreamModules=${skip_downstream}"
+ local scalpel_args="-Dscalpel.enabled=true -Dscalpel.mode=report
-Dscalpel.explain=true -Dscalpel.fullBuildTriggers=
-Dscalpel.fetchBaseBranch=false -Dscalpel.baseBranch=${base_branch}
-Dscalpel.excludePaths=.github/**
-Dscalpel.skipTestsForDownstreamModules=${skip_downstream}"
# Verify merge base is reachable (pre-fetched by the CI workflow step)
if ! git merge-base HEAD "${base_branch}" >/dev/null 2>&1; then
@@ -314,23 +315,22 @@ 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 shadow comparison data — read directly from report fields (0.4.x):
+ # - testedModulesCount: modules whose tests will run (report field, avoids
manual counting)
+ # - reactorModuleCount: total reactor size for N-of-M display (report field,
0.4.1+)
+ # - Per-module lists derived from affectedModules (for the comment detail
sections)
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")
+ # testedModulesCount and reactorModuleCount are written directly by Scalpel
(0.4.x report fields)
+ scalpel_tested_count=$(jq '.testedModulesCount // (.affectedModules |
map(select(.testsSkipped != true)) | length)' "$report" 2>/dev/null || echo "0")
+ scalpel_reactor_count=$(jq '.reactorModuleCount // 0' "$report" 2>/dev/null
|| echo "0")
local mod_count
mod_count=$(jq '.affectedModules | length' "$report" 2>/dev/null || echo "0")
- 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 " Scalpel detected $mod_count affected modules ($scalpel_tested_count
would be tested, reactor: $scalpel_reactor_count)"
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"
@@ -427,6 +427,7 @@ checkManualItTests() {
writeScalpelComparison() {
local comment_file="$1"
local current_reactor_ids="${2:-}"
+ local report="${3:-target/scalpel-report.json}"
# If Scalpel failed, show why in the PR comment
if [ -n "$scalpel_failure_reason" ]; then
@@ -448,19 +449,16 @@ writeScalpelComparison() {
return
fi
- # Count Scalpel modules
+ # Counts — use report fields directly (0.4.x); fall back to counting lists
for older reports
local scalpel_total=0
- local scalpel_test_count=0
local scalpel_skip_count=0
if [ -n "$scalpel_module_ids" ]; then
scalpel_total=$(echo "$scalpel_module_ids" | tr ',' '\n' | grep -c . ||
true)
fi
- 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
+ # scalpel_tested_count and scalpel_reactor_count already set from report
fields in runScalpelDetection
# Compare Scalpel vs current reactor (file-path + grep + -amd expansion)
local current_total=0
@@ -490,8 +488,14 @@ writeScalpelComparison() {
only_current_count=$current_total
fi
- # One-line summary: what Scalpel would change
- local summary="Scalpel: ${scalpel_test_count} tested, ${scalpel_skip_count}
compile-only — current: ${current_total} all tested"
+ # N-of-M framing: use reactorModuleCount from report when available
+ local nm_suffix=""
+ if [ "$scalpel_reactor_count" -gt 0 ] 2>/dev/null; then
+ nm_suffix=" of ${scalpel_reactor_count}"
+ fi
+
+ # One-line summary with N-of-M framing
+ local summary="Scalpel: ${scalpel_tested_count}${nm_suffix} tested,
${scalpel_skip_count} compile-only — current: ${current_total} all tested"
echo "" >> "$comment_file"
echo "---" >> "$comment_file"
@@ -502,8 +506,25 @@ writeScalpelComparison() {
echo "[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected
**${scalpel_total} affected modules** (current approach: ${current_total})." >>
"$comment_file"
echo "" >> "$comment_file"
- # Show modules only Scalpel found (not in current reactor)
+ # Show Scalpel-detected change details
+ if [ -n "$scalpel_props" ]; then
+ echo "Changed properties: ${scalpel_props}" >> "$comment_file"
+ echo "" >> "$comment_file"
+ fi
+ if [ -n "$scalpel_managed_deps" ]; then
+ echo "Changed managed dependencies: ${scalpel_managed_deps}" >>
"$comment_file"
+ echo "" >> "$comment_file"
+ fi
+ if [ -n "$scalpel_managed_plugins" ]; then
+ echo "Changed managed plugins: ${scalpel_managed_plugins}" >>
"$comment_file"
+ echo "" >> "$comment_file"
+ fi
+
+ echo "**Skip-tests mode would test ${scalpel_tested_count} modules**
(${scalpel_direct_count} direct + ${scalpel_downstream_tested} downstream),
**skip tests for ${scalpel_skip_count}** (generated code, meta-modules)" >>
"$comment_file"
+
+ # Show modules only Scalpel found (not in current reactor) — with explain
evidence
if [ "$only_scalpel_count" -gt 0 ]; then
+ echo "" >> "$comment_file"
echo "<details><summary>:warning: Modules only in Scalpel
(${only_scalpel_count})</summary>" >> "$comment_file"
echo "" >> "$comment_file"
echo "$only_in_scalpel" | while read -r m; do
@@ -516,6 +537,7 @@ writeScalpelComparison() {
# Show modules only current approach found (not in Scalpel)
if [ "$only_current_count" -gt 0 ]; then
+ echo "" >> "$comment_file"
echo "<details><summary>Modules only in current approach
(${only_current_count})</summary>" >> "$comment_file"
echo "" >> "$comment_file"
echo "$only_in_current" | while read -r m; do
@@ -526,29 +548,23 @@ writeScalpelComparison() {
echo "" >> "$comment_file"
fi
- # Show Scalpel-detected change details
- if [ -n "$scalpel_props" ]; then
- echo "Changed properties: ${scalpel_props}" >> "$comment_file"
- echo "" >> "$comment_file"
- fi
- if [ -n "$scalpel_managed_deps" ]; then
- echo "Changed managed dependencies: ${scalpel_managed_deps}" >>
"$comment_file"
- echo "" >> "$comment_file"
- fi
- if [ -n "$scalpel_managed_plugins" ]; then
- echo "Changed managed plugins: ${scalpel_managed_plugins}" >>
"$comment_file"
- echo "" >> "$comment_file"
- fi
-
- echo "**Skip-tests mode would test ${scalpel_test_count} modules**
(${scalpel_direct_count} direct + ${scalpel_downstream_tested} downstream),
**skip tests for ${scalpel_skip_count}** (generated code, meta-modules)" >>
"$comment_file"
-
- # Show which modules Scalpel would test
+ # Show which modules Scalpel would test, with per-module evidence
(explain=true)
if [ -n "$scalpel_would_test" ]; then
echo "" >> "$comment_file"
- echo "<details><summary>Modules Scalpel would test
(${scalpel_test_count})</summary>" >> "$comment_file"
+ echo "<details><summary>Modules Scalpel would test
(${scalpel_tested_count})</summary>" >> "$comment_file"
echo "" >> "$comment_file"
echo "$scalpel_would_test" | tr ',' '\n' | while read -r m; do
- [ -n "$m" ] && echo "- \`$m\`" >> "$comment_file"
+ if [ -n "$m" ]; then
+ # Pull evidence[] for this module from the report (explain=true
populates it)
+ local evidence=""
+ evidence=$(jq -r --arg art "$m" '
+ .affectedModules[]
+ | select(.artifactId == $art and (.testsSkipped != true))
+ | (.evidence // [])
+ | if length > 0 then " ← " + join(", ") else "" end
+ ' "$report" 2>/dev/null | head -1 || true)
+ echo "- \`$m\`${evidence}" >> "$comment_file"
+ fi
done
echo "" >> "$comment_file"
echo "</details>" >> "$comment_file"
@@ -721,6 +737,8 @@ main() {
scalpel_direct_count="0"
scalpel_downstream_tested="0"
scalpel_downstream_skipped="0"
+ scalpel_tested_count="0"
+ scalpel_reactor_count="0"
scalpel_failure_reason=""
# Step 2a: Grep-based detection (existing approach)
@@ -977,7 +995,7 @@ main() {
fi
done)
fi
- writeScalpelComparison "$comment_file" "$tested_reactor_ids"
+ writeScalpelComparison "$comment_file" "$tested_reactor_ids"
"target/scalpel-report.json"
# Check for tests disabled in CI via @DisabledIfSystemProperty(named =
"ci.env.name")
local disabled_tests
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index f437e147b91f..d5e447684d28 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -23,6 +23,6 @@
<extension>
<groupId>eu.maveniverse.maven.scalpel</groupId>
<artifactId>extension3</artifactId>
- <version>0.4.1</version>
+ <version>0.4.2</version>
</extension>
</extensions>