Skip to content
Closed
22 changes: 20 additions & 2 deletions .github/CI-ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
134 changes: 105 additions & 29 deletions .github/actions/incremental-build/incremental-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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) ───────────
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 "<details><summary>:microscope: Scalpel shadow comparison (skip-tests mode)</summary>" >> "$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 "<details><summary>Modules Scalpel would test (${scalpel_test_count})</summary>" >> "$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 "</details>" >> "$comment_file"
fi

# Show which modules would have tests skipped
if [ -n "$scalpel_would_skip" ]; then
echo "" >> "$comment_file"
echo "<details><summary>Modules with tests skipped (${scalpel_skip_count})</summary>" >> "$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 "</details>" >> "$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 "</details>" >> "$comment_file"
}

# ── Comment generation ─────────────────────────────────────────────────

writeComment() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/pr-build-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/sonar-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .mvn/extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
<extension>
<groupId>eu.maveniverse.maven.scalpel</groupId>
<artifactId>extension3</artifactId>
<version>0.3.0</version>
<version>0.3.3</version>
</extension>
</extensions>
2 changes: 1 addition & 1 deletion parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@
<juniversalchardet-version>1.0.3</juniversalchardet-version>
<jxmpp-version>1.1.0</jxmpp-version>
<jython-version>2.7.4</jython-version>
<kafka-version>4.2.0</kafka-version>
<kafka-version>4.1.2</kafka-version>
<keycloak-client-version>26.0.8</keycloak-client-version>
<kubernetes-client-version>7.6.1</kubernetes-client-version>
<kudu-version>1.18.1</kudu-version>
Expand Down
Loading