ci: in-process SpotBugs + changed-modules-only reactor in the spotbugs lane #3713
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 their analysis is | |
| # skipped, and exports SPOTBUGS_SCOPE_ARGS (-pl <changed> -am) so only the | |
| # changed modules and their upstream deps build at all (skip-injected deps | |
| # compile for the aux-classpath but are not analysed). A build/config change -> | |
| # full scan, full reactor. 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). | |
| # spotbugs.fork=false analyses in the Maven JVM instead of forking a fresh 2 GB | |
| # JVM per module (59 forks); the shared heap is governed by MAVEN_OPTS above | |
| # (the plugin's maxHeap applies to forks only). | |
| # Skipped entirely when the scope step kept no modules (e.g. a docs-only | |
| # PR): every module would carry spotbugs.skip, so the compile output | |
| # would be unused. The gate below relaxes on the same condition. | |
| if: env.SPOTBUGS_KEPT != '0' | |
| run: | | |
| mvn -T 2C -f ./ddk-parent/pom.xml ${SPOTBUGS_SCOPE_ARGS:-} --batch-mode --fail-never \ | |
| compile \ | |
| spotbugs:spotbugs \ | |
| -Dspotbugs.sarifOutput=true \ | |
| -Dspotbugs.fork=false \ | |
| -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. | |
| # Each scanned source-bearing module must additionally have produced its | |
| # own SARIF, so a partially-dead scoped build cannot hide either. | |
| run: | | |
| set -eu | |
| if [ "${SPOTBUGS_KEPT:-}" = "0" ]; then | |
| echo "All modules skip-injected (no reactor module changed) — nothing to scan." | |
| exit 0 | |
| fi | |
| for mod in ${SPOTBUGS_EXPECT_REPORTS:-}; do | |
| if [ ! -s "${mod}/target/spotbugsSarif.json" ]; then | |
| echo "::error::${mod} was scanned but produced no SpotBugs SARIF — a build failure was swallowed by --fail-never." | |
| exit 1 | |
| fi | |
| done | |
| 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 |