mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
## Summary This pull request restructures Gradle dependency caching across the GitHub Actions workflows. The central `gradle-cache-prime` job is responsible for preparing the shared backend Gradle cache. Reusable workflows restore that shared cache without writing to the same key, while independently triggered workflows use isolated cache namespaces. ## What changed ### Shared Gradle cache - Added a stable `gradle-v1-` cache namespace for the shared backend cache. - The cache key includes the runner OS, runner architecture, JDK version, and the relevant Gradle configuration files. - The cache key is calculated before Gradle runs and reused for the later save step. - The prime job performs a lookup first and resolves backend dependencies only when the exact cache is missing. - This prevents Gradle or Spotless changes during the prime step from producing a different save key from the key used by downstream jobs. ### Reusable workflows - Backend, OpenAPI, license, Docker, E2E, and migration workflows restore the shared cache instead of writing to the shared key. - The backend build matrix includes `matrix.jdk-version` in its cache key. - Enterprise, Tauri, and generated-model workflows support the `use_shared_cache` boolean input. - When `use_shared_cache` is enabled, those workflows restore the shared cache. - When it is disabled, they use workflow-specific cache namespaces. ### Independent workflows Independent workflows now use separate cache prefixes, including: - `gradle-license-report-v1-` - `gradle-swagger-v1-` - `gradle-push-docker-v1-` - `gradle-tauri-releases-v1-` - `gradle-deploy-pr-v1-` - `gradle-playwright-e2e-v1-` - `gradle-generated-models-v1-` This prevents them from creating or affecting the shared backend cache before the prime job. ### Build and E2E flow - Removed the `-PnoSpotless` option from the central Gradle dependency-resolution command. - Removed the separate Gradle dependency prime/retry logic from the live E2E workflow. - Connected the Tauri build and generated-models check to the central cache-prime job. ## Motivation Previously, multiple workflows could use and save the same Gradle cache key independently. The first workflow to save the cache could therefore determine its contents, even if it had resolved a different or incomplete set of dependencies. The cache key was also evaluated after some Gradle tasks had run. If Gradle or Spotless modified a file covered by `hashFiles(...)`, the save key could differ from the restore key used by downstream jobs. This change gives the shared cache a single owner, isolates workflow-specific caches, and makes cache usage deterministic across the CI pipeline. ## Expected result - `gradle-cache-prime` is the single writer for the shared backend Gradle cache. - Downstream jobs restore the same cache without competing cache writes. - Independently triggered workflows remain isolated through their own cache namespaces. - Changes to the monitored Gradle configuration files produce a new cache key. - The normal Gradle/Spotless path is included when the shared cache is populated. ## Validation - Compared the cache key expressions and `hashFiles(...)` inputs across the affected workflows. - Verified that the central restore and save steps use the same precomputed key. - CI should confirm that the prime job populates the shared cache and downstream workflows only restore it. ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my changes - [ ] I have run the relevant CI checks - [ ] I have tested the workflow changes
229 lines
11 KiB
YAML
229 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@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Restore cache Gradle User Home
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
engine/pyproject.toml
|
|
engine/uv.lock
|
|
|
|
# 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: |
|
|
uv run --project engine --locked --group tools 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: |
|
|
uv run --project engine --locked --group tools 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: |
|
|
uv run --project engine --locked --group tools 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
|