mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 05:42:58 +02:00
* 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>
296 lines
12 KiB
YAML
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
|