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 3c48bdbb5f75 ci: add Scalpel-based POM dependency detection alongside
grep
3c48bdbb5f75 is described below
commit 3c48bdbb5f75ed81be01eb7d44117f19bab6d22b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Apr 9 16:53:20 2026 +0200
ci: add Scalpel-based POM dependency detection alongside grep
Add Maveniverse Scalpel 0.1.0 permanently to .mvn/extensions.xml as a
parallel POM dependency detection mechanism in incremental-build.sh. Scalpel
compares effective POM models between base and PR branches, catching managed
dependencies, plugin version changes, BOM imports, and transitive dependency
impacts that the existing grep approach misses. Both detection methods run in
parallel; results are merged (union) before testing. If Scalpel fails, the
script falls back to grep-only. On [...]
---
.github/CI-ARCHITECTURE.md | 43 ++++--
.../actions/incremental-build/incremental-build.sh | 164 ++++++++++++++++++---
.mvn/extensions.xml | 5 +
3 files changed, 183 insertions(+), 29 deletions(-)
diff --git a/.github/CI-ARCHITECTURE.md b/.github/CI-ARCHITECTURE.md
index 72982d383d7d..4672dcd5a9c3 100644
--- a/.github/CI-ARCHITECTURE.md
+++ b/.github/CI-ARCHITECTURE.md
@@ -90,10 +90,14 @@ PR comment: /component-test kafka http
The core test runner. Determines which modules to test using:
1. **File-path analysis**: Maps changed files to Maven modules
-2. **POM dependency analysis**: For `parent/pom.xml` changes, detects property
changes and finds modules that reference the affected properties in their
`pom.xml` files (uses simple grep, not Maveniverse Toolbox — see Known
Limitations below)
+2. **POM dependency analysis** (dual detection):
+ - **Grep-based**: For `parent/pom.xml` changes, detects property changes
and finds modules that explicitly reference the affected properties via
`${property}` in their `pom.xml` files
+ - **Scalpel-based**: Uses [Maveniverse
Scalpel](https://github.com/maveniverse/scalpel) (Maven extension) for
effective POM model comparison — catches managed dependencies, plugin version
changes, BOM imports, and transitive dependency impacts that the grep approach
misses
3. **Extra modules**: Additional modules passed via `/component-test`
-Results are merged, deduplicated, and tested. The script also:
+Both detection methods run in parallel. Their results are merged (union),
deduplicated, and tested. If Scalpel fails (build error, runtime error), the
script falls back to grep-only with no regression.
+
+The script also:
- Detects tests disabled in CI (`@DisabledIfSystemProperty(named =
"ci.env.name")`)
- Applies an exclusion list for generated/meta modules
@@ -119,21 +123,40 @@ Installs system packages required for the build.
The CI sets `-Dci.env.name=github.com` via `MVND_OPTS` (in `install-mvnd`).
Tests can use `@DisabledIfSystemProperty(named = "ci.env.name")` to skip flaky
tests in CI. The test comment warns about these skipped tests.
-## Known Limitations of POM Dependency Detection
+## POM Dependency Detection: Dual Approach
+
+### Grep-based detection (legacy)
+
+The grep approach searches for `${property-name}` references in module
`pom.xml` files. It has known limitations:
+
+1. **Managed dependencies without explicit `<version>`** — Modules inheriting
versions via `<dependencyManagement>` without declaring
`<version>${property}</version>` are missed.
+2. **Maven plugin version changes** — Plugin version properties consumed in
`parent/pom.xml` via `<pluginManagement>` are invisible to child modules.
+3. **BOM imports** — Modules using artifacts from a BOM are not linked to the
BOM version property.
+4. **Transitive dependency changes** — Only direct property references are
detected.
+5. **Non-property version changes** — Structural `<dependencyManagement>`
edits without property substitution are not caught.
+
+### Scalpel-based detection (new)
+
+[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) is a Maven core
extension that compares effective POM models between the base branch and the
PR. It resolves all 5 grep limitations by:
-The property-grep approach has structural limitations that can cause missed
modules:
+- Reading old POM files from the merge-base commit (via JGit)
+- Comparing properties, managed dependencies, and managed plugins between old
and new POMs
+- Resolving the full transitive dependency graph to find all affected modules
+- Detecting plugin version changes via `project.getBuildPlugins()` comparison
-1. **Managed dependencies without explicit** `<version>` — Most Camel modules
inherit dependency versions via `<dependencyManagement>` in the parent POM and
do not declare `<version>${property}</version>` themselves. When a managed
dependency version property changes, only modules that explicitly reference the
property are detected — modules relying on inheritance are missed.
+Scalpel runs in **report mode** (`-Dscalpel.mode=report`), writing a JSON
report to `target/scalpel-report.json` without modifying the Maven reactor. The
report includes affected modules with reasons (`SOURCE_CHANGE`, `POM_CHANGE`,
`TRANSITIVE_DEPENDENCY`, `MANAGED_PLUGIN`).
-2. **Maven plugin version changes are completely invisible** — Plugin version
properties (e.g. `<maven-surefire-plugin-version>`) are both defined and
consumed in `parent/pom.xml` via `<pluginManagement>`. Since the module search
excludes `parent/pom.xml`, no modules are found and **no tests run at all** for
plugin updates. Modules inherit plugins from the parent without any
`${property}` reference in their own `pom.xml`.
+### Dual-detection strategy
-3. **BOM imports** — When a BOM version property changes (e.g.
`<spring-boot-bom-version>`), modules using artifacts from that BOM are not
detected because they reference the BOM's artifacts, not the BOM property.
+Both methods run in parallel. Results are merged (union) before testing. This
lets us:
-4. **Transitive dependency changes** — Modules affected only via transitive
dependencies are not detected.
+1. **Validate Scalpel** — Compare what each method detects across many PRs
+2. **No regression** — If Scalpel fails, grep results are still used
+3. **Gradual migration** — Once Scalpel is validated, grep can be removed
-5. **Non-property version changes** — Direct edits to `<version>` values (not
using `${property}` substitution) or structural changes to
`<dependencyManagement>` sections are not caught.
+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.
-These limitations mean the incremental build may under-test when parent POM
properties change. A future improvement could use [Maveniverse
Toolbox](https://github.com/maveniverse/toolbox) `tree-find` or
[Scalpel](https://github.com/maveniverse/scalpel) to resolve the full
dependency graph and detect all affected modules.
+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).
## Manual Integration Test Advisories
diff --git a/.github/actions/incremental-build/incremental-build.sh
b/.github/actions/incremental-build/incremental-build.sh
index 1938b7c8cefb..d2f01ae420a4 100755
--- a/.github/actions/incremental-build/incremental-build.sh
+++ b/.github/actions/incremental-build/incremental-build.sh
@@ -19,10 +19,14 @@
#
# Determines which modules to test by:
# 1. File-path analysis: maps changed files to their Maven modules
-# 2. POM dependency analysis: for changed pom.xml files, detects property
-# changes and finds modules that reference the affected properties
+# 2. POM dependency analysis (dual detection):
+# a. Grep-based: detects property changes in parent/pom.xml and finds
+# modules that explicitly reference the affected properties
+# b. Scalpel-based: uses Maveniverse Scalpel extension for effective POM
+# model comparison — catches managed deps, plugin changes, BOM imports,
+# and transitive dependency impacts that grep misses
#
-# Both sets of affected modules are merged and deduplicated before testing.
+# All sets of affected modules are merged and deduplicated before testing.
set -euo pipefail
@@ -185,6 +189,84 @@ analyzePomDependencies() {
done <<< "$changed_props"
}
+# ── POM dependency analysis via Scalpel (parallel) ─────────────────────
+#
+# Uses Maveniverse Scalpel (Maven extension) for effective POM model
+# comparison. Detects changed properties, managed dependencies, managed
+# plugins, and transitive dependency impacts that the grep approach misses.
+# Runs alongside grep — results are merged (union) for testing.
+# See https://github.com/maveniverse/scalpel
+
+# Run Scalpel in report mode to detect modules affected by POM changes.
+# Sets caller-visible variables: scalpel_module_ids, scalpel_module_paths,
+# scalpel_props, scalpel_managed_deps, scalpel_managed_plugins
+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).
+ # 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.mode=report -Dscalpel.fullBuildTriggers=
-Dscalpel.alsoMake=false -Dscalpel.alsoMakeDependents=false"
+ # For workflow_dispatch, GITHUB_BASE_REF may not be set
+ if [ -z "${GITHUB_BASE_REF:-}" ]; then
+ scalpel_args="$scalpel_args -Dscalpel.baseBranch=origin/main"
+ fi
+
+ echo " Scalpel: running mvn validate (report mode)..."
+ ./mvnw -B -q validate $scalpel_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
+ return
+ }
+
+ # Parse the Scalpel report
+ local report="target/scalpel-report.json"
+ 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
+ return
+ fi
+
+ # Check if full build was triggered
+ local full_build
+ full_build=$(jq -r '.fullBuildTriggered' "$report")
+ if [ "$full_build" = "true" ]; then
+ local trigger_file
+ trigger_file=$(jq -r '.triggerFile // "unknown"' "$report")
+ echo " Scalpel: Full build triggered by change to $trigger_file"
+ return
+ fi
+
+ # Extract affected module artifactIds (colon-prefixed for Maven -pl
compatibility)
+ scalpel_module_ids=$(jq -r '.affectedModules[].artifactId' "$report"
2>/dev/null | sort -u | sed 's/^/:/' | tr '\n' ',' | sed 's/,$//' || true)
+ scalpel_module_paths=$(jq -r '.affectedModules[].path' "$report" 2>/dev/null
| sort -u | tr '\n' ',' | sed 's/,$//' || true)
+ scalpel_props=$(jq -r '(.changedProperties // []) | if length > 0 then
join(", ") else "" end' "$report" 2>/dev/null || true)
+ 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)
+
+ local mod_count
+ mod_count=$(jq '.affectedModules | length' "$report" 2>/dev/null || echo "0")
+ echo " Scalpel detected $mod_count affected modules"
+ if [ -n "$scalpel_props" ]; then
+ echo " Changed properties: $scalpel_props"
+ fi
+ if [ -n "$scalpel_managed_deps" ]; then
+ echo " Changed managed deps: $scalpel_managed_deps"
+ fi
+ if [ -n "$scalpel_managed_plugins" ]; then
+ echo " Changed managed plugins: $scalpel_managed_plugins"
+ fi
+}
+
# ── Disabled-test detection ─────────────────────────────────────────────
# Scan tested modules for @DisabledIfSystemProperty(named = "ci.env.name")
@@ -269,6 +351,8 @@ writeComment() {
local changed_props_summary="$4"
local testedDependents="$5"
local extra_modules="$6"
+ local managed_deps_summary="${7:-}"
+ local managed_plugins_summary="${8:-}"
echo "<!-- ci-tested-modules -->" > "$comment_file"
@@ -288,21 +372,33 @@ writeComment() {
# Section 2: pom dependency-detected modules
if [ -n "$dep_ids" ]; then
+ echo "" >> "$comment_file"
+ echo ":white_check_mark: **POM dependency changes: targeted tests
included**" >> "$comment_file"
echo "" >> "$comment_file"
if [ -n "$changed_props_summary" ]; then
- echo ":white_check_mark: **POM dependency changes: targeted tests
included**" >> "$comment_file"
- echo "" >> "$comment_file"
echo "Changed properties: ${changed_props_summary}" >> "$comment_file"
echo "" >> "$comment_file"
- local dep_count
- dep_count=$(echo "$dep_ids" | tr ',' '\n' | wc -l | tr -d ' ')
- echo "<details><summary>Modules affected by dependency changes
(${dep_count})</summary>" >> "$comment_file"
+ fi
+ if [ -n "$managed_deps_summary" ]; then
+ echo "Changed managed dependencies: ${managed_deps_summary}" >>
"$comment_file"
echo "" >> "$comment_file"
- echo "$dep_ids" | tr ',' '\n' | while read -r m; do
- echo "- \`$m\`" >> "$comment_file"
- done
+ fi
+ if [ -n "$managed_plugins_summary" ]; then
+ echo "Changed managed plugins: ${managed_plugins_summary}" >>
"$comment_file"
echo "" >> "$comment_file"
- echo "</details>" >> "$comment_file"
+ fi
+ local dep_count
+ dep_count=$(echo "$dep_ids" | tr ',' '\n' | wc -l | tr -d ' ')
+ echo "<details><summary>Modules affected by dependency changes
(${dep_count})</summary>" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo "$dep_ids" | tr ',' '\n' | while read -r m; do
+ echo "- \`$m\`" >> "$comment_file"
+ done
+ echo "" >> "$comment_file"
+ echo "</details>" >> "$comment_file"
+ if [ -n "$managed_deps_summary" ] || [ -n "$managed_plugins_summary" ];
then
+ echo "" >> "$comment_file"
+ echo "> :microscope: Detected via [Maveniverse
Scalpel](https://github.com/maveniverse/scalpel) effective POM comparison" >>
"$comment_file"
fi
fi
@@ -389,20 +485,27 @@ main() {
done
pl="${pl:1}" # strip leading comma
- # Only analyze parent/pom.xml for dependency detection
- # (matches original detect-test.sh behavior; detection improvements deferred
to follow-up PR)
+ # Only analyze parent/pom.xml for grep-based dependency detection
+ # (matches original detect-test.sh behavior)
if echo "$diff_body" | grep -q '^diff --git a/parent/pom.xml'; then
pom_files="parent/pom.xml"
fi
- # ── Step 2: POM dependency analysis ──
+ # ── Step 2: POM dependency analysis (dual: grep + Scalpel) ──
# Variables shared with analyzePomDependencies/findAffectedModules
local dep_module_ids=""
local all_changed_props=""
-
+ # Scalpel results (not local — set by runScalpelDetection)
+ scalpel_module_ids=""
+ scalpel_module_paths=""
+ scalpel_props=""
+ scalpel_managed_deps=""
+ scalpel_managed_plugins=""
+
+ # Step 2a: Grep-based detection (existing approach)
if [ -n "$pom_files" ]; then
echo ""
- echo "Analyzing parent POM dependency changes..."
+ echo "Analyzing parent POM dependency changes (grep)..."
while read -r pom_file; do
[ -z "$pom_file" ] && continue
@@ -421,6 +524,29 @@ main() {
done <<< "$pom_files"
fi
+ # Step 2b: Scalpel detection (parallel, for any pom.xml change)
+ # Scalpel uses effective POM model comparison — catches managed deps,
+ # plugin changes, and transitive impacts that grep misses.
+ if echo "$diff_body" | grep -q '^diff --git a/.*pom\.xml'; then
+ echo ""
+ echo "Running Scalpel POM analysis..."
+ runScalpelDetection
+ fi
+
+ # Step 2c: Merge grep and Scalpel results (union, deduplicated)
+ if [ -n "$scalpel_module_ids" ]; then
+ dep_module_ids="${dep_module_ids:+${dep_module_ids},}${scalpel_module_ids}"
+ dep_module_ids=$(echo "$dep_module_ids" | tr ',' '\n' | sort -u | tr '\n'
',' | sed 's/,$//')
+ fi
+ if [ -n "$scalpel_props" ]; then
+ if [ -z "$all_changed_props" ]; then
+ all_changed_props="$scalpel_props"
+ else
+ # Merge and deduplicate property names
+ all_changed_props=$(printf '%s, %s' "$all_changed_props"
"$scalpel_props" | tr ',' '\n' | sed 's/^ *//' | sort -u | tr '\n' ',' | sed
's/,$//' | sed 's/,/, /g')
+ fi
+ fi
+
# ── Step 3: Merge and deduplicate ──
# Separate file-path modules into testable (has src/test) and pom-only.
# Pom-only modules (e.g. "parent") are kept in the build list but must NOT
@@ -458,7 +584,7 @@ main() {
if [ -z "$final_pl" ]; then
echo ""
echo "No modules to test"
- writeComment "incremental-test-comment.md" "" "" "" "" ""
+ writeComment "incremental-test-comment.md" "" "" "" "" "" "" ""
exit 0
fi
@@ -546,7 +672,7 @@ main() {
# ── Step 5: Write comment and summary ──
local comment_file="incremental-test-comment.md"
- writeComment "$comment_file" "$pl" "$dep_module_ids" "$all_changed_props"
"$testedDependents" "$extraModules"
+ writeComment "$comment_file" "$pl" "$dep_module_ids" "$all_changed_props"
"$testedDependents" "$extraModules" "$scalpel_managed_deps"
"$scalpel_managed_plugins"
# 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 fff4f4d46edc..9a8476957eb2 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -20,4 +20,9 @@
<artifactId>common-custom-user-data-maven-extension</artifactId>
<version>2.1.0</version>
</extension>
+ <extension>
+ <groupId>eu.maveniverse.maven.scalpel</groupId>
+ <artifactId>extension3</artifactId>
+ <version>0.1.0</version>
+ </extension>
</extensions>