Skip to content

Commit f08c43c

Browse files
joaodinissfclaude
andcommitted
ci: scope PMD/CPD/Checkstyle to changed modules in the lint lane
compute-spotbugs-skip.sh becomes compute-analysis-skip.sh with a mode argument: `spotbugs` injects spotbugs.skip as before, `lint` injects pmd.skip, cpd.skip and checkstyle.skip, and each mode exports its -pl/-am reactor scope args. The lint lane gains the scope step and passes LINT_SCOPE_ARGS to both invocations; `compile` stays in the PMD/Checkstyle invocation because PMD's type-resolving rules need Tycho's aux-classpath (skip-injected -am dependencies compile but are not analysed). Changes under ddk-configuration (rulesets, filters) now also trigger the full-scan fail-safe in both lanes. Code Scanning note: repo-wide alert state reflects the default branch, which receives no lint/spotbugs analyses (verify runs on pull_request only), so a scoped upload that omits unchanged modules can only affect PR-context annotations — the same property the spotbugs category has had since the per-module skip landed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6f8b3e6 commit f08c43c

2 files changed

Lines changed: 102 additions & 46 deletions

File tree

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
11
#!/usr/bin/env bash
22
#
3-
# Scope SpotBugs to a pull request's changed modules.
3+
# Scope static analysis (SpotBugs, or PMD/CPD/Checkstyle) to a pull request's
4+
# changed modules.
45
#
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.
6+
# Default is RUN (analyze). On a PR this injects <TOOL.skip>true</TOOL.skip>
7+
# properties into every UNCHANGED reactor module's pom, so the analysis mojos
8+
# skip those modules — for SpotBugs that also skips the per-module JVM fork
9+
# (SpotBugsMojo gates on `skip` before forking). A changed module is still
10+
# analysed with its complete aux-classpath: the -am-pulled unchanged
11+
# dependencies compile but are not analysed. Master/snapshot builds run a full
12+
# scan; this script is invoked on pull_request only.
1113
#
1214
# Why this and not -Dspotbugs.onlyAnalyze: onlyAnalyze is one clean flag, but SpotBugs
1315
# applies its class screener too late (after the per-module fork + class scan), so it
1416
# only trimmed ~17% of the goal vs ~88% for this per-module skip (measured on this
1517
# reactor). A small upstream SpotBugs early-exit (skip the run when no application class
1618
# matches the screener) would make onlyAnalyze competitive; if that ever lands, switch
17-
# to onlyAnalyze and delete this script (tracked in #1455 / spotbugs/spotbugs#3796).
19+
# to onlyAnalyze and delete the spotbugs mode here (tracked in #1455 /
20+
# spotbugs/spotbugs#3796).
1821
#
19-
# On top of the skips, the changed reactor modules are exported as SPOTBUGS_SCOPE_ARGS
20-
# ("-pl <changed> -am") so the lane builds only those modules plus their upstream
21-
# dependencies instead of the full reactor. The -am-pulled unchanged dependencies still
22-
# carry the injected skip: they compile (complete aux-classpath) but are not analysed.
22+
# On top of the skips, the changed reactor modules are exported as
23+
# SPOTBUGS_SCOPE_ARGS / LINT_SCOPE_ARGS ("-pl <changed> -am") so the lane builds
24+
# only those modules plus their upstream dependencies instead of the full reactor.
25+
# The lane's gate cross-checks <MODE>_KEPT / <MODE>_EXPECT_REPORTS so a build
26+
# failure swallowed by --fail-never can never pass as "nothing to scan".
2327
#
24-
# Run from the repository root. Usage: compute-spotbugs-skip.sh <base-sha>
28+
# Run from the repository root. Usage: compute-analysis-skip.sh <base-sha> <spotbugs|lint>
2529
set -euo pipefail
2630
base="${1:?base sha required}"
31+
mode="${2:?mode required: spotbugs|lint}"
32+
33+
case "$mode" in
34+
spotbugs) props="spotbugs.skip"; prefix="SPOTBUGS" ;;
35+
lint) props="pmd.skip cpd.skip checkstyle.skip"; prefix="LINT" ;;
36+
*) echo "unknown mode: $mode" >&2; exit 2 ;;
37+
esac
2738

2839
changed=$(git diff --name-only --diff-filter=ACMR "${base}...HEAD")
2940

@@ -45,8 +56,7 @@ ${module_dirs}
4556
EOF
4657

4758
# 1) A change to shared build/config can affect any module -> full scan (skip nothing).
48-
# ddk-configuration holds the analyzers' rulesets and filters (e.g. the SpotBugs
49-
# exclusion-filter), so a change there must re-scan everything, not skip silently.
59+
# ddk-configuration holds the analyzers' rulesets and filters, so it counts too.
5060
# ddk-target defines the target platform every module resolves against.
5161
# Fail safe: the worst case here is "analyse everything", never "analyse nothing".
5262
while IFS= read -r f; do
@@ -60,10 +70,10 @@ while IFS= read -r f; do
6070
# full scan analysed all of them (not just >=1) — a mojo death swallowed by
6171
# --fail-never can't pass as long as one sibling reported.
6272
if [ -n "${GITHUB_ENV:-}" ]; then
63-
echo "SPOTBUGS_KEPT=all" >> "$GITHUB_ENV"
64-
echo "SPOTBUGS_EXPECT_REPORTS=${all_source_modules}" >> "$GITHUB_ENV"
73+
echo "${prefix}_KEPT=all" >> "$GITHUB_ENV"
74+
echo "${prefix}_EXPECT_REPORTS=${all_source_modules}" >> "$GITHUB_ENV"
6575
fi
66-
echo "Build/config change ($f) -> full SpotBugs scan (no skips)."
76+
echo "Build/config change ($f) -> full ${mode} scan (no skips)."
6777
exit 0
6878
;;
6979
esac
@@ -76,18 +86,20 @@ EOF
7686
# grep's no-match exit would otherwise kill the script under pipefail.
7787
changed_mods=$(printf '%s\n' "${changed}" | { grep '/' || true; } | cut -d/ -f1 | sort -u)
7888

79-
# 3) Idempotently inject the skip property; handle poms with and without <properties>.
89+
# 3) Idempotently inject the skip properties; handle poms with and without <properties>.
8090
# sed -i.bak + rm is portable across GNU (CI) and BSD (local) sed.
8191
inject_skip() {
82-
local pom="$1/pom.xml"
92+
local pom="$1/pom.xml" prop
8393
[ -f "$pom" ] || return 0
84-
if grep -q '<spotbugs\.skip>' "$pom"; then return 0; fi
85-
if grep -q '<properties>' "$pom"; then
86-
sed -i.bak 's#<properties>#<properties>\n <spotbugs.skip>true</spotbugs.skip>#' "$pom"
87-
else
88-
sed -i.bak 's#</project># <properties>\n <spotbugs.skip>true</spotbugs.skip>\n </properties>\n</project>#' "$pom"
89-
fi
90-
rm -f "$pom.bak"
94+
for prop in $props; do
95+
if grep -q "<${prop//./\\.}>" "$pom"; then continue; fi
96+
if grep -q '<properties>' "$pom"; then
97+
sed -i.bak "s#<properties>#<properties>\n <${prop}>true</${prop}>#" "$pom"
98+
else
99+
sed -i.bak "s#</project># <properties>\n <${prop}>true</${prop}>\n </properties>\n</project>#" "$pom"
100+
fi
101+
rm -f "$pom.bak"
102+
done
91103
}
92104

93105
# 4) Skip every reactor module that was not touched by this PR. Kept modules with a
@@ -103,7 +115,7 @@ while IFS= read -r mod; do
103115
kept=$((kept + 1))
104116
kept_pl="${kept_pl:+${kept_pl},}../${mod}"
105117
# Only bundles with sources reliably emit a report (a source-less bundle,
106-
# e.g. pure branding, has nothing for the analyzer to write a SARIF about).
118+
# e.g. pure branding, has nothing for PMD to write a SARIF about).
107119
if [ -f "${mod}/META-INF/MANIFEST.MF" ] && [ -d "${mod}/src" ]; then
108120
expect_reports="${expect_reports:+${expect_reports} }${mod}"
109121
fi
@@ -115,6 +127,13 @@ done <<EOF
115127
${module_dirs}
116128
EOF
117129

130+
# 5) Scope the reactor to the changed modules + their upstream dependencies. ddk-target
131+
# is always kept in the -pl list: the target-definition artifact is referenced by
132+
# target-platform-configuration, not by any MANIFEST, so -am never pulls it — without
133+
# it in the reactor Tycho falls back to a local-repository copy, which fails on a
134+
# cold cache and can silently resolve a stale target definition on a warm one.
135+
# With no analysable changed module (docs-only, or source-less-only) no scope args
136+
# are exported; the workflow skips the lane's Maven step(s) entirely on <MODE>_KEPT=0.
118137
# The gate's presence check needs to distinguish "all modules skip-injected"
119138
# (zero reports is the expected state) from "the analysis silently died".
120139
# If the only changed modules are source-less (feature / target / repository — nothing
@@ -127,25 +146,18 @@ else
127146
effective_kept=$kept
128147
fi
129148
if [ -n "${GITHUB_ENV:-}" ]; then
130-
echo "SPOTBUGS_KEPT=${effective_kept}" >> "$GITHUB_ENV"
149+
echo "${prefix}_KEPT=${effective_kept}" >> "$GITHUB_ENV"
131150
fi
132151

133-
# 5) Scope the reactor to the changed modules + their upstream dependencies. ddk-target
134-
# is always kept in the -pl list: the target-definition artifact is referenced by
135-
# target-platform-configuration, not by any MANIFEST, so -am never pulls it — without
136-
# it in the reactor Tycho falls back to a local-repository copy, which fails on a
137-
# cold cache and can silently resolve a stale target definition on a warm one.
138-
# With no analysable changed module (docs-only, or source-less-only) no scope args
139-
# are exported; the workflow skips the Maven step entirely on SPOTBUGS_KEPT=0.
140152
if [ "$effective_kept" -gt 0 ] && [ -n "${GITHUB_ENV:-}" ]; then
141-
echo "SPOTBUGS_SCOPE_ARGS=-pl ../ddk-target,${kept_pl} -am" >> "$GITHUB_ENV"
142-
echo "SPOTBUGS_EXPECT_REPORTS=${expect_reports}" >> "$GITHUB_ENV"
153+
echo "${prefix}_SCOPE_ARGS=-pl ../ddk-target,${kept_pl} -am" >> "$GITHUB_ENV"
154+
echo "${prefix}_EXPECT_REPORTS=${expect_reports}" >> "$GITHUB_ENV"
143155
fi
144156

145-
echo "SpotBugs scope: scanning ${effective_kept} changed module(s), skipping ${skipped} unchanged."
157+
echo "${mode} scope: scanning ${effective_kept} changed module(s), skipping ${skipped} unchanged."
146158
echo "Changed modules: ${changed_mods:-<none>}"
147159
if [ "$kept" -gt 0 ] && [ "$effective_kept" -eq 0 ]; then
148-
echo "Only source-less modules changed (no analysable sources) -> no-op (KEPT=0)."
160+
echo "Only source-less modules changed (no analysable sources) -> no-op (${prefix}_KEPT=0)."
149161
fi
150162
if [ "$effective_kept" -gt 0 ]; then
151163
echo "Reactor scope args: -pl ../ddk-target,${kept_pl} -am"

.github/workflows/verify.yml

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
runs-on: ubuntu-24.04
2929
steps:
3030
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
31+
with:
32+
fetch-depth: 0 # need the PR base commit to diff the changed modules
3133
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
3234
with:
3335
distribution: 'temurin'
@@ -43,17 +45,33 @@ jobs:
4345
key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }}
4446
restore-keys: ${{ runner.os }}-maven-publish-
4547

48+
- name: Scope static analysis to the PR's changed modules
49+
# Injects pmd/cpd/checkstyle skip properties into unchanged module poms and
50+
# exports LINT_SCOPE_ARGS (-pl <changed> -am) so only the changed modules and
51+
# their upstream deps build (skip-injected deps compile for PMD's type
52+
# resolution but are not analysed). Build/config change -> full scan, full
53+
# reactor. pull_request only; master/snapshot run a full scan.
54+
run: bash .github/scripts/compute-analysis-skip.sh "${{ github.event.pull_request.base.sha }}" lint
55+
4656
- name: PMD + Checkstyle reports (SARIF)
4757
# PMD: SarifRenderer FQCN — emits pmd.sarif.json AND keeps pmd.xml.
4858
# Checkstyle: output.format=sarif — SARIF content in checkstyle-result.xml.
4959
# CPD is excluded here: the global -Dformat flag uses PMD's Renderer
5060
# hierarchy and would ClassCastException CPD's CPDReportRenderer.
61+
# `compile` stays: PMD's type-resolving rules need Tycho's aux-classpath.
62+
# jgit.dirtyWorkingTree=ignore: the scope step edits poms (see the spotbugs
63+
# lane for the rationale; this job releases nothing).
64+
# Skipped entirely when the scope step kept no modules (e.g. a docs-only
65+
# PR): every module would carry the skip properties, so the compile
66+
# output would be unused. The gate below relaxes on the same condition.
67+
if: env.LINT_KEPT != '0'
5168
run: |
52-
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
69+
mvn -T 2C -f ./ddk-parent/pom.xml ${LINT_SCOPE_ARGS:-} --batch-mode --fail-never \
5370
compile \
5471
pmd:pmd checkstyle:checkstyle \
5572
-Dformat=net.sourceforge.pmd.renderers.SarifRenderer \
56-
-Dcheckstyle.output.format=sarif
73+
-Dcheckstyle.output.format=sarif \
74+
-Djgit.dirtyWorkingTree=ignore
5775
5876
- name: CPD report (separate invocation — no SARIF support)
5977
# CPD has no SARIF renderer; emits cpd.xml only. Run standalone so the
@@ -63,8 +81,11 @@ jobs:
6381
# compile pass (verified at the current token threshold and at 100).
6482
# NOTE: the CPD token threshold is governed by pmd.cpd.min in
6583
# ddk-parent/pom.xml (#1339; tuned to 100 by #1397).
84+
# No jgit flag needed: a direct goal invocation runs no lifecycle, so the
85+
# build-qualifier's dirty-tree check never executes here.
86+
if: env.LINT_KEPT != '0'
6687
run: |
67-
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
88+
mvn -T 2C -f ./ddk-parent/pom.xml ${LINT_SCOPE_ARGS:-} --batch-mode --fail-never \
6889
pmd:cpd-check
6990
7091
- name: Merge per-module SARIFs (PMD + Checkstyle)
@@ -101,8 +122,29 @@ jobs:
101122
# merge() only writes its output when it found at least one valid input,
102123
# so a missing merged file means that analyzer silently died (e.g. a
103124
# plugin bump broke a renderer flag) — never a clean pass.
125+
# Exception: the scope step skip-injected every module (no reactor module
126+
# changed), where zero reports is the expected state. Each scanned
127+
# source-bearing module must additionally have produced its own PMD
128+
# SARIF, cpd.xml, and Checkstyle SARIF, so a partially-dead scoped
129+
# build cannot hide either.
104130
run: |
105131
set -eu
132+
if [ "${LINT_KEPT:-}" = "0" ]; then
133+
echo "All modules skip-injected (no reactor module changed) — nothing to lint."
134+
exit 0
135+
fi
136+
for mod in ${LINT_EXPECT_REPORTS:-}; do
137+
for rep in pmd.sarif.json cpd.xml; do
138+
if [ ! -s "${mod}/target/${rep}" ]; then
139+
echo "::error::${mod} was scanned but produced no ${rep} — a build failure was swallowed by --fail-never."
140+
exit 1
141+
fi
142+
done
143+
if ! jq -e . "${mod}/target/checkstyle-result.xml" >/dev/null 2>&1; then
144+
echo "::error::${mod} was scanned but produced no valid Checkstyle SARIF — a build failure was swallowed by --fail-never."
145+
exit 1
146+
fi
147+
done
106148
for f in .sarif-merged/pmd.sarif .sarif-merged/checkstyle.sarif; do
107149
if [ ! -s "$f" ]; then
108150
echo "::error::No valid SARIF input produced for ${f##*/} — the analysis silently failed."
@@ -116,7 +158,7 @@ jobs:
116158
sarif_total=$(jq '[.runs[].results[]?] | length' \
117159
.sarif-merged/pmd.sarif .sarif-merged/checkstyle.sarif 2>/dev/null \
118160
| awk '{s+=$1} END {print s+0}')
119-
cpd_total=$(find . -name 'cpd.xml' -path '*/target/*' -exec grep -c '<duplication ' {} + 2>/dev/null \
161+
cpd_total=$(find . -name 'cpd.xml' -path '*/target/*' -exec grep -cH '<duplication ' {} + 2>/dev/null \
120162
| awk -F: '{s+=$2} END {print s+0}')
121163
echo "PMD/Checkstyle SARIF violations: $sarif_total"
122164
echo "CPD duplications: $cpd_total"
@@ -126,7 +168,9 @@ jobs:
126168
fi
127169
128170
- name: Upload PMD/Checkstyle SARIF to Code Scanning
129-
if: always()
171+
# Skip when nothing was scanned (e.g. a docs-only PR -> all modules skipped):
172+
# an empty .sarif-merged would otherwise fail upload-sarif ("No SARIF files").
173+
if: ${{ always() && hashFiles('.sarif-merged/pmd.sarif', '.sarif-merged/checkstyle.sarif') != '' }}
130174
# Annotation-only, never the gate: a fork PR gets a read-only token and
131175
# upload-sarif 403s, which must not red an otherwise-clean lane.
132176
continue-on-error: true
@@ -166,7 +210,7 @@ jobs:
166210
# changed modules and their upstream deps build at all (skip-injected deps
167211
# compile for the aux-classpath but are not analysed). A build/config change ->
168212
# full scan, full reactor. pull_request only; master/snapshot run a full scan.
169-
run: bash .github/scripts/compute-spotbugs-skip.sh "${{ github.event.pull_request.base.sha }}"
213+
run: bash .github/scripts/compute-analysis-skip.sh "${{ github.event.pull_request.base.sha }}" spotbugs
170214

171215
- name: SpotBugs report (SARIF)
172216
# sarifOutput=true emits spotbugsSarif.json (also writes spotbugsXml.xml).

0 commit comments

Comments
 (0)