Skip to content

Commit 8ea8cbc

Browse files
joaodinissfclaude
andcommitted
ci: scope SpotBugs to the PR's changed modules (per-module skip)
SpotBugs' per-module analysis is the spotbugs job's long pole. A PR only needs its changed modules scanned, so a pre-step injects <spotbugs.skip>true> into every unchanged reactor module's pom — the plugin then skips the goal, and the per-module JVM fork, for them. The full-reactor compile is kept (a changed module keeps its complete aux-classpath); a build/config change falls back to a full scan. pull_request only — master/snapshot run a full scan. -Dspotbugs.onlyAnalyze was the cleaner-looking alternative but screens too late (after the per-module fork), ~17% vs ~88% measured; the script header documents the migration if an upstream SpotBugs early-exit ever lands. - .github/scripts/compute-spotbugs-skip.sh: diff -> changed modules -> inject skip into the unchanged ones (idempotent; build/config change -> full scan). - verify.yml spotbugs job: fetch-depth 0 + a scope step before compile; -Djgit.dirtyWorkingTree=ignore because the scope step dirties poms on purpose and this job releases nothing (releases/maven-verify keep =error); SARIF upload guarded so an empty scan set (no module scanned) doesn't fail the upload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 49f7655 commit 8ea8cbc

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Scope SpotBugs to a pull request's changed modules.
4+
#
5+
# Default is RUN (analyze). On a PR this injects <spotbugs.skip>true</spotbugs.skip>
6+
# into every UNCHANGED reactor module's pom, so spotbugs-maven-plugin skips the goal —
7+
# and therefore the per-module JVM fork (SpotBugsMojo gates on `skip` before forking) —
8+
# for those modules. The full-reactor compile is left intact (a changed module is still
9+
# analysed with its complete aux-classpath). Master/snapshot builds run a full scan;
10+
# this script is invoked on pull_request only.
11+
#
12+
# Why this and not -Dspotbugs.onlyAnalyze: onlyAnalyze is one clean flag, but SpotBugs
13+
# applies its class screener too late (after the per-module fork + class scan), so it
14+
# only trimmed ~17% of the goal vs ~88% for this per-module skip (measured on this
15+
# reactor). A small upstream SpotBugs early-exit (skip the run when no application class
16+
# matches the screener) would make onlyAnalyze competitive; if that ever lands, switch
17+
# to onlyAnalyze and delete this script.
18+
#
19+
# Run from the repository root. Usage: compute-spotbugs-skip.sh <base-sha>
20+
set -euo pipefail
21+
base="${1:?base sha required}"
22+
23+
changed=$(git diff --name-only --diff-filter=ACMR "${base}...HEAD")
24+
25+
# 1) A change to shared build/config can affect any module -> full scan (skip nothing).
26+
# ddk-configuration holds the analyzers' rulesets and filters (e.g. the SpotBugs
27+
# exclusion-filter), so a change there must re-scan everything, not skip silently.
28+
# Fail safe: the worst case here is "analyse everything", never "analyse nothing".
29+
while IFS= read -r f; do
30+
[ -n "$f" ] || continue
31+
case "$f" in
32+
pom.xml | ddk-parent/* | .mvn/* | *.target | .github/* | ddk-configuration/* | *[Ss]pot[Bb]ugs*[Ee]xclude*)
33+
echo "Build/config change ($f) -> full SpotBugs scan (no skips)."
34+
exit 0
35+
;;
36+
esac
37+
done <<EOF
38+
${changed}
39+
EOF
40+
41+
# 2) Changed top-level module directories (the reactor module == top-level dir here).
42+
# `|| true`: a PR touching only root files (e.g. README.md) has no '/' paths;
43+
# grep's no-match exit would otherwise kill the script under pipefail.
44+
changed_mods=$(printf '%s\n' "${changed}" | { grep '/' || true; } | cut -d/ -f1 | sort -u)
45+
46+
# 3) Reactor module dirs from ddk-parent's <modules> (strip the leading ../).
47+
# ddk-parent is NOT in its own <modules>, so it can never be skip-injected — which
48+
# is what prevents an accidental inherited (global) skip.
49+
module_dirs=$(grep -oE '<module>\.\./[^<]+</module>' ddk-parent/pom.xml \
50+
| sed -E 's#.*\.\./([^<]+)</module>#\1#')
51+
52+
# 4) Idempotently inject the skip property; handle poms with and without <properties>.
53+
# sed -i.bak + rm is portable across GNU (CI) and BSD (local) sed.
54+
inject_skip() {
55+
local pom="$1/pom.xml"
56+
[ -f "$pom" ] || return 0
57+
if grep -q '<spotbugs\.skip>' "$pom"; then return 0; fi
58+
if grep -q '<properties>' "$pom"; then
59+
sed -i.bak 's#<properties>#<properties>\n <spotbugs.skip>true</spotbugs.skip>#' "$pom"
60+
else
61+
sed -i.bak 's#</project># <properties>\n <spotbugs.skip>true</spotbugs.skip>\n </properties>\n</project>#' "$pom"
62+
fi
63+
rm -f "$pom.bak"
64+
}
65+
66+
# 5) Skip every reactor module that was not touched by this PR.
67+
kept=0
68+
skipped=0
69+
while IFS= read -r mod; do
70+
[ -n "$mod" ] || continue
71+
if printf '%s\n' "${changed_mods}" | grep -qx "$mod"; then
72+
kept=$((kept + 1))
73+
else
74+
inject_skip "$mod"
75+
skipped=$((skipped + 1))
76+
fi
77+
done <<EOF
78+
${module_dirs}
79+
EOF
80+
81+
# The gate's presence check needs to distinguish "all modules skip-injected"
82+
# (zero reports is the expected state) from "the analysis silently died".
83+
if [ -n "${GITHUB_ENV:-}" ]; then
84+
echo "SPOTBUGS_KEPT=${kept}" >> "$GITHUB_ENV"
85+
fi
86+
87+
echo "SpotBugs scope: scanning ${kept} changed module(s), skipping ${skipped} unchanged."
88+
echo "Changed modules: ${changed_mods:-<none>}"

.github/workflows/verify.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ jobs:
141141
MAVEN_OPTS: -Xmx4g
142142
steps:
143143
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
144+
with:
145+
fetch-depth: 0 # need the PR base commit to diff the changed modules
144146
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
145147
with:
146148
distribution: 'temurin'
@@ -156,13 +158,25 @@ jobs:
156158
key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }}
157159
restore-keys: ${{ runner.os }}-maven-publish-
158160

161+
- name: Scope SpotBugs to the PR's changed modules
162+
# Injects <spotbugs.skip>true> into unchanged module poms so the per-module
163+
# SpotBugs fork is skipped for them (the lever that actually scopes the cost).
164+
# Full compile is preserved (correct aux-classpath); a build/config change ->
165+
# full scan. pull_request only; master/snapshot run a full scan.
166+
run: bash .github/scripts/compute-spotbugs-skip.sh "${{ github.event.pull_request.base.sha }}"
167+
159168
- name: SpotBugs report (SARIF)
160169
# sarifOutput=true emits spotbugsSarif.json (also writes spotbugsXml.xml).
170+
# jgit.dirtyWorkingTree=ignore: the scope step intentionally edits poms, so the
171+
# working tree is dirty here; this job releases nothing, so we tell Tycho's jgit
172+
# build-qualifier to use the last commit's timestamp instead of failing (the
173+
# repo keeps jgit.dirtyWorkingTree=error for maven-verify / releases).
161174
run: |
162175
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
163176
compile \
164177
spotbugs:spotbugs \
165-
-Dspotbugs.sarifOutput=true
178+
-Dspotbugs.sarifOutput=true \
179+
-Djgit.dirtyWorkingTree=ignore
166180
167181
- name: Merge per-module SpotBugs SARIFs
168182
if: always()
@@ -186,8 +200,14 @@ jobs:
186200
- name: Gate on SpotBugs violations
187201
# A missing merged SARIF means the analysis silently died (--fail-never
188202
# suppresses even compile/resolution failures) — never a clean pass.
203+
# Exception: the scope step skip-injected every module (no reactor module
204+
# changed, e.g. a docs-only PR), where zero reports is the expected state.
189205
run: |
190206
set -eu
207+
if [ "${SPOTBUGS_KEPT:-}" = "0" ]; then
208+
echo "All modules skip-injected (no reactor module changed) — nothing to scan."
209+
exit 0
210+
fi
191211
if [ ! -s .sarif-merged/spotbugs.sarif ]; then
192212
echo "::error::No SpotBugs SARIF produced — the analysis silently failed."
193213
exit 1
@@ -200,7 +220,9 @@ jobs:
200220
fi
201221
202222
- name: Upload SpotBugs SARIF to Code Scanning
203-
if: always()
223+
# Skip when nothing was scanned (e.g. a docs-only PR -> all modules skipped):
224+
# an empty .sarif-merged would otherwise fail upload-sarif ("No SARIF files").
225+
if: ${{ always() && hashFiles('.sarif-merged/spotbugs.sarif') != '' }}
204226
# Annotation-only, never the gate: a fork PR gets a read-only token and
205227
# upload-sarif 403s, which must not red an otherwise-clean lane.
206228
continue-on-error: true

0 commit comments

Comments
 (0)