diff --git a/.github/workflows/pr-compile-check.yml b/.github/workflows/pr-compile-check.yml index 654c0b99..4199cde9 100644 --- a/.github/workflows/pr-compile-check.yml +++ b/.github/workflows/pr-compile-check.yml @@ -41,6 +41,64 @@ jobs: - name: Compile run: ./gradlew -S classes testClasses + # Report only: both tools run with ignoreFailures, so a finding cannot fail this step + # -- a crash of the tool itself still does, which is wanted. The findings used to exist + # only in the console log; since #110 they are files, and the table and the artifact + # below are what makes them readable from a pull request. + - name: Static analysis (report only) + run: ./gradlew -S pmdMain spotbugsMain + + # Counted from the report XML, never from Gradle's output or exit code. A missing + # report prints "no report" and does not fail the step: that case is already visible + # in the step above. + - name: Summarise the static analysis + if: always() + run: | + set -euo pipefail + shopt -s nullglob + + count() { + local file="$1" element="$2" + if [ -f "$file" ]; then + # grep exits 1 on zero matches, which pipefail would turn into a failed step. + { grep -o "<$element " "$file" || true; } | wc -l | tr -d ' ' + else + echo "no report" + fi + } + + { + echo "### Static analysis" + echo "" + echo "| module | PMD violations | SpotBugs bugs |" + echo "|---|---|---|" + } >> "$GITHUB_STEP_SUMMARY" + + for module in core app-desktop i18n-generator; do + pmd=$(count "$module/build/reports/pmd/main.xml" violation) + spotbugs=$(count "$module/build/reports/spotbugs/main.xml" BugInstance) + echo "$module: pmd=$pmd spotbugs=$spotbugs" + echo "| \`$module\` | $pmd | $spotbugs |" >> "$GITHUB_STEP_SUMMARY" + done + + { + echo "" + echo "Report only: nothing fails on a finding. The XML and HTML reports are the" + echo "static-analysis artifact of this run. i18n-generator is Kotlin only, so PMD" + echo "has no Java source there and writes no report." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Keep the static analysis reports + if: always() + uses: actions/upload-artifact@v6 + with: + name: static-analysis + path: | + */build/reports/pmd/ + */build/reports/spotbugs/ + if-no-files-found: ignore + retention-days: 14 + # What a translation PR is actually about, put where its author will look. The counts # come from the files rather than from Gradle's output, so an up-to-date task cannot # make the table disappear. diff --git a/AGENTS.md b/AGENTS.md index 9da07732..9ac2f89e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,7 +129,7 @@ Important: a test failure fails the build, but PMD and SpotBugs run with `ignore - inspect the Gradle test summary; - inspect the test-results XML under `*/build/test-results/` when needed; - do not infer "all tests passed" from exit code 0 -- read the XML; -- report PMD/SpotBugs findings that are visible in the relevant build. +- report PMD/SpotBugs findings that are visible in the relevant build. Both tools write XML and HTML: `/build/reports/pmd/main.{xml,html}` and `/build/reports/spotbugs/main.{xml,html}` (since #110; before that SpotBugs printed to the console only). Count from the XML (`/build/reports/spotbugs/main.{xml,html} (#110). + tasks.withType().configureEach { + reports.create("xml") { required.set(true) } + reports.create("html") { required.set(true) } + } + /* * Coverage is a report, not a gate: there is no jacocoTestCoverageVerification and no * minimum percentage. A threshold set at today's number would trip on the next unrelated diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 53672add..5cbdeabf 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -202,6 +202,7 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen - JUnit 5/Mockito, PMD and SpotBugs are part of the verification environment. JUnit and Mockito are test-only dependencies; nothing of them reaches the shipped app image. - Test classes live under `src/test` only. Until #62 a few JUnit classes sat under `src/main`, compiled on every build and were never executed; that is the historical reason the two modules once declared JUnit and Mockito as `implementation` dependencies. - A test failure fails the build. Only PMD and SpotBugs run with `ignoreFailures`, so their findings do not. Still read the test-result XML under `*/build/test-results/test/` rather than inferring a pass from the exit code. +- Static analysis is a report, not a gate, and since #110 both tools leave a file behind: PMD under `/build/reports/pmd/main.{xml,html}`, SpotBugs under `/build/reports/spotbugs/main.{xml,html}` (the plugin names the report after the source set, not the task). The PR workflow runs `pmdMain spotbugsMain`, counts `