Files
kst4contest/.github/workflows/pr-compile-check.yml
T
Rsclub2_2andClaude Fable 5.1 f2b0fdd5b4 Write SpotBugs reports and publish static analysis on pull requests (#110) (#114)
* build: write SpotBugs XML and HTML reports (#110)

The SpotBugs tasks had no report configured, so findings went to stdout
only and, with ignoreFailures, scrolled past unread. Every SpotBugsTask
now writes XML (for CI to count) and HTML (for people) to the plugin's
default location, <module>/build/reports/spotbugs/main.{xml,html}.
ignoreFailures stays; the build still passes with findings present.

Fixes #110

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: run PMD and SpotBugs on pull requests and publish the reports (#110)

The compile job runs pmdMain and spotbugsMain after compiling (report
only: both tools have ignoreFailures, a crash of a tool still fails),
counts <violation> and <BugInstance> elements per module from the report
XML into the job summary, and uploads */build/reports/pmd and
*/build/reports/spotbugs as the static-analysis artifact for 14 days.
A missing report prints "no report" without failing the step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: name the PMD and SpotBugs report files and the CI artifact (#110)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 12:49:08 +02:00

296 lines
12 KiB
YAML

name: PR Compile Check
on:
pull_request:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
compile:
name: Compile (Java 21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
# classes + testClasses rather than compileJava + compileTestJava. The old pair did
# cover Kotlin -- checked with a deliberately broken Kotlin test file, and also in
# --dry-run: the Kotlin plugin makes compileTestJava depend on compileTestKotlin,
# because Java has to see the Kotlin output. But that is the plugin's wiring and not
# this job's statement of intent, and it covered no resources. These two aggregates
# say "compile everything in all three modules" and do not depend on knowing that.
#
# They also run the translation generator, which app-desktop's compileKotlin depends
# on. That is why a pull request contributing a language needs no workflow of its own:
# an orphan key, a dropped placeholder or a duplicate fails here, with the generator's
# own message naming the file.
- 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.
- name: Summarise the interface texts
if: always()
run: |
set -euo pipefail
shopt -s nullglob
base="app-desktop/src/main/i18n/strings_en.properties"
if [ ! -f "$base" ]; then
echo "no base translation file at $base" >&2
exit 1
fi
count_keys() { grep -cE '^[^#!][^=]*=' "$1" || true; }
total=$(count_keys "$base")
{
echo "### Interface texts"
echo ""
echo "| language | translated | of | missing |"
echo "|---|---|---|---|"
} >> "$GITHUB_STEP_SUMMARY"
for file in app-desktop/src/main/i18n/strings_*.properties; do
lang=$(basename "$file" .properties); lang=${lang#strings_}
have=$(count_keys "$file")
missing=$(( total - have ))
[ "$missing" -lt 0 ] && missing=0
echo "| \`$lang\` | $have | $total | $missing |" >> "$GITHUB_STEP_SUMMARY"
done
{
echo ""
echo "A language may be incomplete: a key it leaves out falls back to English,"
echo "which is what lets a translation be contributed a piece at a time. German is"
echo "held complete by a test, because it ships."
} >> "$GITHUB_STEP_SUMMARY"
worksheets=(app-desktop/build/generated/i18n/todo/*.todo.properties)
if [ ${#worksheets[@]} -gt 0 ]; then
{
echo ""
echo "The build wrote a worksheet of what is still missing for:"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
for w in "${worksheets[@]}"; do
echo "- \`$(basename "$w")\`" >> "$GITHUB_STEP_SUMMARY"
done
fi
test:
name: Test (Java 21, headless)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
# Skiko's native library links against libGL, so it has to be loadable even though
# nothing here renders through it: the drawing tests use Skia's software backend and
# runComposeUiTest never opens a window. Without this the whole Compose half of the
# suite dies with "libskiko-linux-x64.so: libGL.so.1: cannot open shared object
# file" -- which reads like a missing display and is not one. Found by running this
# job under act, where the container has no libGL; GitHub's own runner image happens
# to ship it, so the step is what makes the job reproducible off GitHub.
- name: Install the OpenGL client library skiko links against
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libgl1
# No Xvfb, deliberately. Everything that pins behaviour runs without a display:
# runComposeUiTest reaches the semantics tree headless (measured before this job was
# written), and the ImageComposeScene drawing tests render in software. The handful
# of tests that open a real window guard themselves with assumeFalse(isHeadless) and
# skip here; the summary below prints the skip count, so that is visible rather than
# assumed.
#
# Software GL under Xvfb was considered and rejected: a missing Skia GL context is
# the hardest failure in this codebase to read, and importing that class of flake
# into CI would cost more than the few window tests are worth here.
# All three modules. The translation generator is a Gradle module and not buildSrc
# precisely so that its 42 tests run here: buildSrc tests do not run on `./gradlew
# build` -- measured, not assumed. The summary below globs */build/test-results, so
# the module was counted the moment it existed.
- name: Test
run: ./gradlew -S test
# Coverage is a report, not a gate: no threshold, nothing in it can fail on a
# percentage. Deliberately not `if: always()` -- without a completed test run the
# report would be partial, and after a failed test the result XML below is what matters.
# jacocoTestReport writes the per-module XML the summary reads; the aggregate merges
# core and app-desktop (i18n-generator is build tooling and stays out).
- name: Coverage report
run: ./gradlew -S jacocoTestReport jacocoAggregatedReport
- name: Summarise the test results
if: always()
run: |
set -euo pipefail
shopt -s nullglob
files=(*/build/test-results/test/TEST-*.xml)
if [ ${#files[@]} -eq 0 ]; then
echo "No test result XML was produced -- the test task did not run." >&2
exit 1
fi
# Counted from the XML and not from Gradle's console output: this project's own
# rule is never to infer "all tests passed" from an exit code.
summary=$(grep -h "<testsuite " "${files[@]}" \
| sed 's/.*tests="\([0-9]*\)".*skipped="\([0-9]*\)".*failures="\([0-9]*\)".*errors="\([0-9]*\)".*/\1 \2 \3 \4/' \
| awk '{t+=$1; s+=$2; f+=$3; e+=$4} END {print t" "s" "f" "e}')
read -r total skipped failures errors <<< "$summary"
echo "tests=$total skipped=$skipped failures=$failures errors=$errors"
{
echo "### Tests"
echo ""
echo "| total | skipped | failures | errors |"
echo "|---|---|---|---|"
echo "| $total | $skipped | $failures | $errors |"
echo ""
echo "Skipped are the tests that need a display, plus the deliberately"
echo "disabled reference-data generator. This job runs headless on purpose."
} >> "$GITHUB_STEP_SUMMARY"
if [ "$failures" != "0" ] || [ "$errors" != "0" ]; then
echo "The result XML reports failures or errors." >&2
exit 1
fi
- name: Summarise the coverage
if: always()
run: |
set -euo pipefail
# The last LINE/BRANCH counter in a JaCoCo XML is the report-level total; the
# per-package and per-class counters come before it.
percent() {
local file="$1" type="$2"
{ grep -o "<counter type=\"$type\"[^>]*/>" "$file" || true; } | tail -1 \
| sed 's/.*missed="\([0-9]*\)".*covered="\([0-9]*\)".*/\1 \2/' \
| awk 'NF == 2 { t=$1+$2; if (t==0) print "n/a"; else printf "%.1f%% (%d/%d)", 100*$2/t, $2, t }'
}
row() {
local label="$1" file="$2"
if [ -f "$file" ]; then
echo "| $label | $(percent "$file" LINE) | $(percent "$file" BRANCH) |"
else
echo "| $label | no report | no report |"
fi
}
{
echo ""
echo "### Coverage"
echo ""
echo "| module | lines | branches |"
echo "|---|---|---|"
row "core" core/build/reports/jacoco/test/jacocoTestReport.xml
row "app-desktop" app-desktop/build/reports/jacoco/test/jacocoTestReport.xml
row "core + app-desktop" build/reports/jacoco/aggregated/jacoco.xml
echo ""
echo "Report only: nothing fails on a percentage. The aggregated HTML report is the"
echo "coverage-report artifact of this run. i18n-generator is build tooling and is"
echo "not counted."
} >> "$GITHUB_STEP_SUMMARY"
- name: Keep the coverage report
if: always()
uses: actions/upload-artifact@v6
with:
name: coverage-report
path: build/reports/jacoco/aggregated/html/
if-no-files-found: ignore
retention-days: 14
- name: Keep the test reports on a failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: test-results
path: |
*/build/test-results/test/
*/build/reports/tests/test/
if-no-files-found: ignore
retention-days: 7