Skip to content

Commit 8ec1682

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 9727257 commit 8ec1682

2 files changed

Lines changed: 113 additions & 2 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
# ddk-target defines the target platform every module resolves against.
29+
# Fail safe: the worst case here is "analyse everything", never "analyse nothing".
30+
while IFS= read -r f; do
31+
[ -n "$f" ] || continue
32+
case "$f" in
33+
pom.xml | ddk-parent/* | .mvn/* | *.target | ddk-target/* | .github/* | ddk-configuration/* | *[Ss]pot[Bb]ugs*[Ee]xclude*)
34+
echo "Build/config change ($f) -> full SpotBugs scan (no skips)."
35+
exit 0
36+
;;
37+
esac
38+
done <<EOF
39+
${changed}
40+
EOF
41+
42+
# 2) Changed top-level module directories (the reactor module == top-level dir here).
43+
# `|| true`: a PR touching only root files (e.g. README.md) has no '/' paths;
44+
# grep's no-match exit would otherwise kill the script under pipefail.
45+
changed_mods=$(printf '%s\n' "${changed}" | { grep '/' || true; } | cut -d/ -f1 | sort -u)
46+
47+
# 3) Reactor module dirs from ddk-parent's <modules> (strip the leading ../).
48+
# ddk-parent is NOT in its own <modules>, so it can never be skip-injected — which
49+
# is what prevents an accidental inherited (global) skip.
50+
module_dirs=$(grep -oE '<module>\.\./[^<]+</module>' ddk-parent/pom.xml \
51+
| sed -E 's#.*\.\./([^<]+)</module>#\1#')
52+
53+
# 4) Idempotently inject the skip property; handle poms with and without <properties>.
54+
# sed -i.bak + rm is portable across GNU (CI) and BSD (local) sed.
55+
inject_skip() {
56+
local pom="$1/pom.xml"
57+
[ -f "$pom" ] || return 0
58+
if grep -q '<spotbugs\.skip>' "$pom"; then return 0; fi
59+
if grep -q '<properties>' "$pom"; then
60+
sed -i.bak 's#<properties>#<properties>\n <spotbugs.skip>true</spotbugs.skip>#' "$pom"
61+
else
62+
sed -i.bak 's#</project># <properties>\n <spotbugs.skip>true</spotbugs.skip>\n </properties>\n</project>#' "$pom"
63+
fi
64+
rm -f "$pom.bak"
65+
}
66+
67+
# 5) Skip every reactor module that was not touched by this PR.
68+
kept=0
69+
skipped=0
70+
while IFS= read -r mod; do
71+
[ -n "$mod" ] || continue
72+
if printf '%s\n' "${changed_mods}" | grep -qx "$mod"; then
73+
kept=$((kept + 1))
74+
else
75+
inject_skip "$mod"
76+
skipped=$((skipped + 1))
77+
fi
78+
done <<EOF
79+
${module_dirs}
80+
EOF
81+
82+
# The gate's presence check needs to distinguish "all modules skip-injected"
83+
# (zero reports is the expected state) from "the analysis silently died".
84+
if [ -n "${GITHUB_ENV:-}" ]; then
85+
echo "SPOTBUGS_KEPT=${kept}" >> "$GITHUB_ENV"
86+
fi
87+
88+
echo "SpotBugs scope: scanning ${kept} changed module(s), skipping ${skipped} unchanged."
89+
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
144+
with:
145+
fetch-depth: 0 # need the PR base commit to diff the changed modules
144146
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # 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)