Skip to content

ci(pull-request-kotlin): persist Gradle build-cache across runs (split restore/save) [NO-JIRA]#298

Merged
JesperTerkelsen merged 1 commit into
mainfrom
ci/kotlin-pr-persist-gradle-cache
Jun 5, 2026
Merged

ci(pull-request-kotlin): persist Gradle build-cache across runs (split restore/save) [NO-JIRA]#298
JesperTerkelsen merged 1 commit into
mainfrom
ci/kotlin-pr-persist-gradle-cache

Conversation

@JesperTerkelsen

@JesperTerkelsen JesperTerkelsen commented Jun 4, 2026

Copy link
Copy Markdown
Member

Problem

The Kotlin PR test job recompiles ~2m 30s of KSP + Kotlin every run, even for unchanged modules. Dependencies are cached, but setup-java's built-in cache: 'gradle' keys on the gradle files and only saves on a cache miss — so Gradle's build-cache (~/.gradle/caches/build-cache-1, the compiled task output) is written once and frozen, and every run recompiles from scratch. The Sonar cache (fixed key ${{ runner.os }}-sonar) has the same freeze.

Fix

Switch both caches to the split restore/save pattern and save on every run under a per-commit, branch-scoped key, so the build-cache persists and unchanged kspKotlin/compileKotlin come back FROM-CACHE.

key:          ${{ runner.os }}-gradle-${{ github.head_ref }}-${{ github.sha }}
restore-keys: ${{ runner.os }}-gradle-${{ github.head_ref }}-   # this branch's latest (tightest hit)
              ${{ runner.os }}-gradle-                           # fallback: most recent run
  • save runs on always() (compiled output is cached even when tests fail) and is skipped on an exact re-run hit.
  • Caches are isolated per repository automatically (GitHub + runs-on), so no ${{ github.repository }} is needed in the key.

Validated on a canary (service-feature #737)

Two runs through this branch ref — cold, then warm (identical source):

Step Cold (run 1) Warm (run 2)
Restore Gradle cache 0s (miss) 10s (hit, ~690 MB @ 260 MB/s)
Run linter (ktlint + compile) 3m 46s 12s
Run tests with coverage 3m 53s 13s
Upload to SonarCloud 52s 36s
Save caches 10s 15s
Total 9m 11s 1m 56s

Log confirms: Cache hit for restore-key: …-gradle-…compileKotlin FROM-CACHE, kspKotlin FROM-CACHE, ktlint… FROM-CACHE.

Caveat on the 1m 56s: run 2 had byte-identical source (empty commit), so even the test task cache-hit. A real PR that changes code still recompiles changed files and re-runs affected tests (~2m 17s of real internal-api test execution). So the dependable win is eliminating the ~2m 30s–3m 40s cold recompile; expect a typical code PR around ~4–5 min (from ~9 min), and a docs/no-op PR the full ~1m 56s.

Notes

  • Build-cache win only materialises for repos with org.gradle.caching=true (harmless no-op otherwise); dependency caching helps every repo.
  • Per-commit keys mean more cache writes — fine on runs-on (no size cap, 10-day lifecycle).
  • Branch isolation: the -gradle- fallback can pull another PR's cache on a cold first build of a new branch — low risk for this PR-only, ships-nothing workflow, and moot if runs-on enforces branch-scoping on the magic cache.

⚠️ Blast radius

pull-request-kotlin.yml@main is consumed by 35 repos — merging flips all of them on their next PR run. Two repos pin a SHA (service-charges, service-internal-ocpi-tooling-service) and are unaffected. Consider migrating a couple via the branch ref first as a wider canary before merging to main.

🤖 Generated with Claude Code

@JesperTerkelsen JesperTerkelsen marked this pull request as ready for review June 4, 2026 20:32
@JesperTerkelsen JesperTerkelsen requested a review from a team as a code owner June 4, 2026 20:32
@JesperTerkelsen JesperTerkelsen requested review from tobias0106 and removed request for a team June 4, 2026 20:32
@JesperTerkelsen JesperTerkelsen force-pushed the ci/kotlin-pr-persist-gradle-cache branch from e6d9c35 to 89e89b9 Compare June 4, 2026 20:49
…plit restore/save [NO-JIRA]

The test job recompiled (~150s of KSP + Kotlin, main+test) on every run.
Dependencies were cached, but setup-java's built-in 'gradle' cache keys on the
gradle files and only saves on a cache MISS — so Gradle's build-cache (compiled
task output) froze at first save and never accumulated, leaving every run to
recompile from scratch.

Switch to the split restore/save pattern and save on every run under a
per-commit key, so the build-cache persists and unchanged KSP/Kotlin output
returns FROM-CACHE instead of recompiling. The Sonar cache had the same
freeze-on-fixed-key problem and gets the same treatment.

Cache key is branch-scoped with a shared fallback: a PR restores its own
branch's latest cache first (closest source -> best build-cache hit rate),
else the most recent run. Caches are isolated per repository automatically
(GitHub + runs-on), so no repo name is needed in the key. Saves run on
always() so compiled output is cached even when tests fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JesperTerkelsen JesperTerkelsen force-pushed the ci/kotlin-pr-persist-gradle-cache branch from 89e89b9 to d430113 Compare June 4, 2026 20:51
@JesperTerkelsen JesperTerkelsen merged commit 0996477 into main Jun 5, 2026
1 check passed
@JesperTerkelsen JesperTerkelsen deleted the ci/kotlin-pr-persist-gradle-cache branch June 5, 2026 05:08
JesperTerkelsen added a commit that referenced this pull request Jun 5, 2026
…anch builds [NO-JIRA] (#303)

GitHub/runs-on caches are branch-scoped: a run can only restore caches from its
own ref + the default branch (main). The Kotlin PR test workflow (#298) never
runs on main, so no Linux-gradle-* cache exists there, and its Linux-gradle-
fallback can't reach sibling PR caches -> every fresh branch's first build is
cold (~3-4m recompile).

sonar-cloud.yml already compiles + tests on main (code-analysis on push:main).
Have it save ~/.gradle under Linux-gradle-main-<sha> (split restore/save,
replacing setup-java's frozen cache:'gradle'). That sits on refs/heads/main =
default branch, so the PR test workflow's existing Linux-gradle- fallback matches
it -> fresh PR branches start warm. No change to pull-request-kotlin.yml needed.

Save is guarded to refs/heads/main so a PR-context run can't poison the baseline.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants