Files
Stirling-PDF/.github/workflows/coverage-aggregate.yml
T
Ludy 2265e48b32 chore(ci): optimize GitHub Actions Gradle caching across workflows (#7299)
# Description of Changes

This PR refactors Gradle caching across the GitHub Actions workflows to
improve cache reuse, reduce dependency resolution overhead, and shorten
CI execution times.

### What was changed

- Replaced multiple `gradle/actions/setup-gradle` steps with a unified
`actions/cache`-based Gradle User Home cache strategy.
- Standardized cache paths across workflows to include:
  - `~/.gradle/caches`
  - `~/.gradle/wrapper`
- Introduced consistent cache keys using:
  - Runner OS
  - Runner architecture
  - JDK version
- Hashes of Gradle wrapper, version catalog, Gradle build files, and
project build scripts.
- Added restore keys to maximize cache hit rates across similar
environments.
- Added a new **`gradle-cache-prime`** job in the main build workflow
that:
  - Restores or creates the shared Gradle cache.
  - Resolves backend dependencies before downstream jobs execute.
  - Makes the populated cache available to subsequent jobs.
- Updated workflow dependencies so Gradle-based jobs wait for the cache
priming job before execution.
- Simplified and unified Gradle cache handling across numerous CI
workflows, including backend builds, OpenAPI generation, database
migration tests, Docker tests, Tauri builds, Swagger generation,
enterprise builds, release workflows, and license generation.
- Updated workflow comments to reflect the new caching strategy and
shared cache behavior.

### Why the change was made

The previous workflows used a mixture of Gradle setup actions and
partial dependency caches, leading to duplicated dependency downloads,
inconsistent cache behavior, and longer CI runtimes. Consolidating all
workflows onto a shared Gradle User Home cache with a dedicated cache
priming job improves cache reuse, reduces unnecessary dependency
resolution, and makes CI execution more consistent.



---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-08-06 11:43:53 +00:00

234 lines
11 KiB
YAML

name: Aggregate backend coverage
# Reusable workflow called from build.yml after every backend coverage
# producer (JUnit, e2e:live, cucumber) has run. Downloads each job's raw
# .exec, merges them into one JaCoCo report, and posts a combined step
# summary alongside the per-source ones.
#
# Kept separate from the per-source jobs so:
# - the per-source jobs stay fast and independent (no cross-job waits)
# - this job can `if: always()` and still produce something useful when
# one of the producers fails partway through
# - frontend producers can be added later without touching the
# producers themselves
on:
workflow_call:
inputs:
frontend-validation-result:
description: Result of the frontend-validation producer job
required: false
type: string
default: skipped
playwright-e2e-live-result:
description: Result of the playwright-e2e-live producer job
required: false
type: string
default: skipped
permissions:
contents: read
jobs:
aggregate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "25"
distribution: "temurin"
- name: Cache Gradle User Home
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
restore-keys: |
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
gradle-${{ runner.os }}-${{ runner.arch }}-
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install defusedxml for coverage scripts
# Both coverage-summary.py and coverage-matrix.py parse JaCoCo
# XML through defusedxml - see the script headers for context.
run: python -m pip install --quiet defusedxml
# Pattern matches every artifact this PR's producers might upload:
# jacoco-exec-junit-jdk-25 (uploaded only by the saas
# leg of backend-build, which
# is a strict superset of the
# core + proprietary legs)
# jacoco-exec-e2e-live
# jacoco-exec-cucumber
# Each lands as a sibling dir under coverage-execs/, with the .exec
# files preserving their original relative paths.
- name: Download all .exec artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: jacoco-exec-*
path: coverage-execs/
merge-multiple: false
continue-on-error: true
- name: Inventory .exec files
id: inventory
# Splits the downloaded artifacts into two buckets:
# * e2e-only = cucumber + Playwright live (user-flow coverage)
# * all = the above plus JUnit (everything we test)
#
# Bucketing is by artifact-name prefix: download-artifact preserves
# the artifact name as the top-level dir, so JUnit's `.exec`s live
# under coverage-execs/jacoco-exec-junit-*/... while the others
# are under coverage-execs/jacoco-exec-{e2e-live,cucumber}/...
#
# If nothing was uploaded (e.g. all producers crashed before
# writing) we exit gracefully so this advisory job never fails CI.
run: |
mapfile -t all_execs < <(find coverage-execs -name '*.exec' -type f | sort)
mapfile -t e2e_execs < <(find coverage-execs -name '*.exec' -type f -not -path '*/jacoco-exec-junit-*' | sort)
if [ "${#all_execs[@]}" -eq 0 ]; then
echo "::warning::No .exec artifacts found - skipping aggregate report"
echo "found_all=false" >> "$GITHUB_OUTPUT"
echo "found_e2e=false" >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'All %d .exec files:\n' "${#all_execs[@]}"
printf ' %s\n' "${all_execs[@]}"
IFS=','; all_joined="${all_execs[*]}"
echo "files_all=$all_joined" >> "$GITHUB_OUTPUT"
echo "found_all=true" >> "$GITHUB_OUTPUT"
if [ "${#e2e_execs[@]}" -eq 0 ]; then
echo "::notice::No e2e/cucumber .exec files - e2e-only report will be skipped"
echo "found_e2e=false" >> "$GITHUB_OUTPUT"
else
printf 'E2E-only %d .exec files:\n' "${#e2e_execs[@]}"
printf ' %s\n' "${e2e_execs[@]}"
unset IFS
IFS=','; e2e_joined="${e2e_execs[*]}"
echo "files_e2e=$e2e_joined" >> "$GITHUB_OUTPUT"
echo "found_e2e=true" >> "$GITHUB_OUTPUT"
fi
- name: Compile classes for JaCoCo class lookup
# jacocoReportFromExec only needs the compiled .class files
# under each subproject's build/classes/java/main/. `classes`
# (compileJava + processResources) is enough; we skipped the
# heavier `assemble` to avoid building bootJar / fat jars that
# add 60+ seconds per run for no gain to the report.
if: steps.inventory.outputs.found_all == 'true'
run: ./gradlew classes -PnoSpotless
- name: Generate e2e-only JaCoCo report
# "Real user-flow" coverage: only counts code reached by an actual
# HTTP request from cucumber or live Playwright. Useful for
# questions like "how much of our backend does a user actually
# hit?". Skipped when neither producer uploaded a .exec.
if: steps.inventory.outputs.found_e2e == 'true'
run: |
./gradlew jacocoReportFromExec \
-PexecFile="${{ steps.inventory.outputs.files_e2e }}" \
-PreportDir=build/reports/jacoco/aggregate-e2e \
-PnoSpotless
- name: Generate combined JaCoCo report (everything)
if: steps.inventory.outputs.found_all == 'true'
run: |
./gradlew jacocoReportFromExec \
-PexecFile="${{ steps.inventory.outputs.files_all }}" \
-PreportDir=build/reports/jacoco/aggregate-all \
-PnoSpotless
- name: E2E-only step summary
# Rendered first so it gets prime real estate in the Summary
# tab - this is the number most readers actually want
# ("how much of the backend do real user flows cover?").
if: steps.inventory.outputs.found_e2e == 'true'
run: |
python scripts/coverage-summary.py \
--title "Real user-flow backend coverage (e2e:live + cucumber)" \
--jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \
--github-step-summary
- name: ALL-sources step summary
# Separate call (not a multi-input one) because the helper's
# rightmost "Aggregate" column would sum the two reports - which
# is meaningless when one is a strict superset of the other.
if: steps.inventory.outputs.found_all == 'true'
run: |
python scripts/coverage-summary.py \
--title "Combined backend coverage (JUnit + e2e:live + cucumber)" \
--jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \
--github-step-summary
- name: Upload combined aggregate report
if: steps.inventory.outputs.found_all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jacoco-aggregate-all-${{ github.run_id }}
path: build/reports/jacoco/aggregate-all/
retention-days: 14
- name: Upload e2e-only aggregate report
if: steps.inventory.outputs.found_e2e == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jacoco-aggregate-e2e-${{ github.run_id }}
path: build/reports/jacoco/aggregate-e2e/
retention-days: 14
# --------------------------------------------------------------
# Per-area matrix: rolls backend + frontend coverage into one
# table indexed by core/proprietary/saas/desktop. Pulls the
# frontend artifacts now (after the JaCoCo step has done its
# work) so the per-source backend summaries still render first
# even if the matrix step fails.
# --------------------------------------------------------------
- name: Download vitest coverage artifact
# frontend-validation uploads as `frontend-coverage`. Tolerate
# absence on backend-only runs by skipping the download entirely
# when the producer job was not part of this workflow run.
if: inputs.frontend-validation-result == 'success'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: frontend-coverage
path: matrix-inputs/vitest/
continue-on-error: true
- name: Download Playwright frontend coverage artifact
# e2e-live uploads the artifact with a stable name. Skip the
# download entirely when the producer job did not run.
if: inputs.playwright-e2e-live-result == 'success'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: playwright-frontend-coverage
path: matrix-inputs/playwright/
continue-on-error: true
- name: Coverage matrix step summary
if: always()
# Matrix references the two aggregate JaCoCo XMLs (already
# generated above) plus whichever frontend artifacts landed.
# Every input is optional; missing ones render as "-".
run: |
python scripts/coverage-matrix.py \
${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \
${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \
--vitest matrix-inputs/vitest/coverage-summary.json \
--playwright-frontend matrix-inputs/playwright/coverage-pw-summary/coverage-summary.json \
--title "Coverage matrix (per-area, e2e vs all)" \
--github-step-summary