Skip to content

ci: scope SpotBugs to a PR's changed modules (per-module skip) #3712

ci: scope SpotBugs to a PR's changed modules (per-module skip)

ci: scope SpotBugs to a PR's changed modules (per-module skip) #3712

Workflow file for this run

name: verify
on:
pull_request:
# Code Scanning needs write access to upload SARIF results for inline annotations.
permissions:
contents: read
security-events: write
jobs:
# Fast, early-fail lint lane: PMD + Checkstyle (+ CPD). Turns red in a few
# minutes on any violation, independent of the long build below, so a stray
# PMD/Checkstyle issue is reported immediately rather than after `verify`.
#
# `compile` is in the same invocation as the analysis goals: PMD's
# type-resolving rules (e.g. InvalidLogMessageFormat on the SLF4J
# trailing-Throwable idiom) need Tycho's aux-classpath, which a fresh `mvn`
# does not inherit from a prior step's target/classes.
#
# `--fail-never` lets every module produce its report (no Maven cascade-skip),
# so the uploaded SARIF — and therefore the inline annotations — are complete.
# The trade-off: --fail-never suppresses even compile and target-resolution
# failures (mvn exits 0 on a broken build), so the gate pairs the jq count
# with a presence check — zero valid analyzer inputs fails the lane rather
# than reading as a clean pass. Compilation itself is independently gated by
# maven-verify.
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Restore Maven dependency cache
# Restore-only, mirroring snapshot.yml's producer cache exactly (path and
# key are hashed into the cache version — see the maven-verify step).
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }}
restore-keys: ${{ runner.os }}-maven-publish-
- name: PMD + Checkstyle reports (SARIF)
# PMD: SarifRenderer FQCN — emits pmd.sarif.json AND keeps pmd.xml.
# Checkstyle: output.format=sarif — SARIF content in checkstyle-result.xml.
# CPD is excluded here: the global -Dformat flag uses PMD's Renderer
# hierarchy and would ClassCastException CPD's CPDReportRenderer.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
pmd:pmd checkstyle:checkstyle \
-Dformat=net.sourceforge.pmd.renderers.SarifRenderer \
-Dcheckstyle.output.format=sarif
- name: CPD report (separate invocation — no SARIF support)
# CPD has no SARIF renderer; emits cpd.xml only. Run standalone so the
# PMD -Dformat flag isn't in scope.
# NOTE: project CPD token threshold is currently very high (issue #1339),
# which effectively disables detection; re-tune once #1339 lands.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
pmd:cpd-check
- name: Merge per-module SARIFs (PMD + Checkstyle)
if: always()
# Code Scanning accepts one run per category per upload; each analyzer
# writes one SARIF per module, so concatenate each analyzer's results
# into a single run under .sarif-merged/.
run: |
mkdir -p .sarif-merged
# Merge per-module SARIFs into one run. Filter to JSON-parseable files:
# the ddk-parent aggregator writes a plain-XML checkstyle-result.xml that
# would break jq, and modules with no findings may emit non-SARIF stubs.
merge() { # $1 = find-glob, $2 = output
local f valid=()
while IFS= read -r f; do
jq -e . "$f" >/dev/null 2>&1 && valid+=("$f")
done < <(find . -path "$1")
if [ ${#valid[@]} -gt 0 ]; then
# del(.ruleIndex): each result's ruleIndex points into its OWN run's
# rules array, but the merge keeps only the first run's tool — Code
# Scanning must resolve rules by ruleId string instead.
jq -s '{
"$schema": .[0]."$schema", version: .[0].version,
runs: [{ tool: .[0].runs[0].tool,
results: [.[].runs[].results[]? | del(.ruleIndex)],
invocations: [.[].runs[].invocations[]?] }]
}' "${valid[@]}" > "$2"
fi
}
merge '*/target/pmd.sarif.json' .sarif-merged/pmd.sarif
merge '*/target/checkstyle-result.xml' .sarif-merged/checkstyle.sarif
- name: Gate on PMD / CPD / Checkstyle violations
# merge() only writes its output when it found at least one valid input,
# so a missing merged file means that analyzer silently died (e.g. a
# plugin bump broke a renderer flag) — never a clean pass.
run: |
set -eu
for f in .sarif-merged/pmd.sarif .sarif-merged/checkstyle.sarif; do
if [ ! -s "$f" ]; then
echo "::error::No valid SARIF input produced for ${f##*/} — the analysis silently failed."
exit 1
fi
done
if [ "$(find . -name 'cpd.xml' -path '*/target/*' | wc -l)" -eq 0 ]; then
echo "::error::No cpd.xml produced — CPD silently failed."
exit 1
fi
sarif_total=$(jq '[.runs[].results[]?] | length' \
.sarif-merged/pmd.sarif .sarif-merged/checkstyle.sarif 2>/dev/null \
| awk '{s+=$1} END {print s+0}')
cpd_total=$(find . -name 'cpd.xml' -path '*/target/*' -exec grep -c '<duplication ' {} + 2>/dev/null \
| awk -F: '{s+=$2} END {print s+0}')
echo "PMD/Checkstyle SARIF violations: $sarif_total"
echo "CPD duplications: $cpd_total"
if [ "$sarif_total" != "0" ] || [ "$cpd_total" != "0" ]; then
echo "::error::Static analysis found violations (PMD/CPD/Checkstyle)."
exit 1
fi
- name: Upload PMD/Checkstyle SARIF to Code Scanning
if: always()
# Annotation-only, never the gate: a fork PR gets a read-only token and
# upload-sarif 403s, which must not red an otherwise-clean lane.
continue-on-error: true
uses: github/codeql-action/upload-sarif@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3.35.4
with:
sarif_file: .sarif-merged
category: lint
# SpotBugs is the slow critical-path analysis (the experiments' durable
# finding), so it runs in its own parallel lane and never delays `lint`.
spotbugs:
runs-on: ubuntu-24.04
env:
MAVEN_OPTS: -Xmx4g
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # need the PR base commit to diff the changed modules
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Restore Maven dependency cache
# Restore-only, mirroring snapshot.yml's producer cache exactly (path and
# key are hashed into the cache version — see the maven-verify step).
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }}
restore-keys: ${{ runner.os }}-maven-publish-
- name: Scope SpotBugs to the PR's changed modules
# Injects <spotbugs.skip>true> into unchanged module poms so the per-module
# SpotBugs fork is skipped for them (the lever that actually scopes the cost).
# Full compile is preserved (correct aux-classpath); a build/config change ->
# full scan. pull_request only; master/snapshot run a full scan.
run: bash .github/scripts/compute-spotbugs-skip.sh "${{ github.event.pull_request.base.sha }}"
- name: SpotBugs report (SARIF)
# sarifOutput=true emits spotbugsSarif.json (also writes spotbugsXml.xml).
# jgit.dirtyWorkingTree=ignore: the scope step intentionally edits poms, so the
# working tree is dirty here; this job releases nothing, so we tell Tycho's jgit
# build-qualifier to use the last commit's timestamp instead of failing (the
# repo keeps jgit.dirtyWorkingTree=error for maven-verify / releases).
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
spotbugs:spotbugs \
-Dspotbugs.sarifOutput=true \
-Djgit.dirtyWorkingTree=ignore
- name: Merge per-module SpotBugs SARIFs
if: always()
run: |
mkdir -p .sarif-merged
valid=()
while IFS= read -r f; do
jq -e . "$f" >/dev/null 2>&1 && valid+=("$f")
done < <(find . -path '*/target/spotbugsSarif.json')
if [ ${#valid[@]} -gt 0 ]; then
# del(.ruleIndex): see the lint merge — indexes are per-run, the
# merged tool keeps only the first run's rules.
jq -s '{
"$schema": .[0]."$schema", version: .[0].version,
runs: [{ tool: .[0].runs[0].tool,
results: [.[].runs[].results[]? | del(.ruleIndex)],
invocations: [.[].runs[].invocations[]?] }]
}' "${valid[@]}" > .sarif-merged/spotbugs.sarif
fi
- name: Gate on SpotBugs violations
# A missing merged SARIF means the analysis silently died (--fail-never
# suppresses even compile/resolution failures) — never a clean pass.
# Exception: the scope step skip-injected every module (no reactor module
# changed, e.g. a docs-only PR), where zero reports is the expected state.
run: |
set -eu
if [ "${SPOTBUGS_KEPT:-}" = "0" ]; then
echo "All modules skip-injected (no reactor module changed) — nothing to scan."
exit 0
fi
if [ ! -s .sarif-merged/spotbugs.sarif ]; then
echo "::error::No SpotBugs SARIF produced — the analysis silently failed."
exit 1
fi
sb_total=$(jq '[.runs[].results[]?] | length' .sarif-merged/spotbugs.sarif 2>/dev/null || echo 0)
echo "SpotBugs SARIF violations: $sb_total"
if [ "$sb_total" != "0" ]; then
echo "::error::SpotBugs found violations."
exit 1
fi
- name: Upload SpotBugs SARIF to Code Scanning
# Skip when nothing was scanned (e.g. a docs-only PR -> all modules skipped):
# an empty .sarif-merged would otherwise fail upload-sarif ("No SARIF files").
if: ${{ always() && hashFiles('.sarif-merged/spotbugs.sarif') != '' }}
# Annotation-only, never the gate: a fork PR gets a read-only token and
# upload-sarif 403s, which must not red an otherwise-clean lane.
continue-on-error: true
uses: github/codeql-action/upload-sarif@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3.35.4
with:
sarif_file: .sarif-merged
category: spotbugs
line-endings:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check LF line endings
run: bash .github/scripts/check-line-endings.sh
# Build + tests only. Static analysis now lives in the `lint` and `spotbugs`
# jobs, so the redundant checkstyle/pmd/spotbugs goals are dropped from here —
# this is the wall-clock long pole and no longer re-runs analysis.
# No `-T 2C`: tests are not known to pass reliably under reactor parallelism.
maven-verify:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up JDK 21
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Log Maven version
run: mvn --version
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Restore Maven dependency cache
# Restore-only: PR scopes cannot share caches with each other, so per-PR
# saves are dead weight that evicts the useful master-scoped caches
# (10 GB repo budget). The producer is snapshot.yml on master pushes
# (Linux-maven-publish-*). Path and key must mirror snapshot.yml exactly:
# the literal path spec is hashed into the cache *version*, so any
# variation (~/.m2 vs /home/runner/.m2) makes its caches unmatchable.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-publish-${{ hashFiles('**/pom.xml', '**/*.target') }}
restore-keys: ${{ runner.os }}-maven-publish-
- name: Build with Maven within a virtual X Server Environment
run: xvfb-run mvn clean verify -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
- name: Fail on missing surefire reports
if: always()
run: bash .github/scripts/check-surefire-reports.sh
- name: Archive Tycho Surefire Plugin
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: tycho-surefire-plugin
path: ${{ env.GITHUB_WORKSPACE }}/com.avaloq.tools.ddk.xtext.test/target/work/data/.metadata/.log