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