Skip to content

feat(component-test): opt-in Gradle build cache for deploy test jobs [ENERGY-2742] - #340

Merged
ckattmann merged 5 commits into
mainfrom
fix/ENERGY-deploy-gradle-cache
Oct 7, 2026
Merged

ckattmann merged 5 commits into
mainfrom
fix/ENERGY-deploy-gradle-cache

Conversation

@ckattmann

@ckattmann ckattmann commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The test job in component-test-kotlin.yml (used by deploy-kotlin.yml and deploy-kotlin-v2.yml) caches Gradle only via actions/setup-java's cache: 'gradle'. That key is a hash of the dependency files and actions/cache skips saving on a primary-key hit, so the ~/.gradle/caches/build-cache-1 inside the archive is a frozen snapshot that never accumulates task outputs.

Every deploy therefore runs all test-job tasks from scratch, including the prod deploy of a SHA that the staging deploy already tested.

Evidence (service-energy, 2026-09-23 to 2026-10-03)

  • Latest prod deploy (run 37143878242, main @ 4443e1b5): Test project 3m31s, BUILD SUCCESSFUL in 3m 30s, 11 actionable tasks: 11 executed, cache log Cache hit occurred on the primary key …, not saving cache.
  • Test project across the last five prod deploys: 3m31s, 4m32s, 4m26s, 3m59s, 3m48s.
  • All 5 prod deploys I checked re-tested a SHA that a staging deploy had already tested (e.g. prod 37143878242 and staging 37105244580 are both 4443e1b5).
  • When task inputs are identical Gradle's build cache covers the whole job: PR run 37032930602 had kspKotlin, compileKotlin, compileTestKotlin, :test and the kover reports all FROM-CACHE, BUILD SUCCESSFUL in 23s.
  • When sources changed it does not help: 8 of 14 recent PR runs with actions/cache restored still ran every task (15 actionable tasks: 15 executed).

Change

Opt-in input cache-gradle-build-outputs (default false, so no behaviour change for existing callers) on component-test-kotlin.yml, forwarded by deploy-kotlin.yml and deploy-kotlin-v2.yml. When true:

  • actions/setup-java runs without cache: gradle
  • Restore Gradle cache (actions/cache/restore) over ~/.gradle/caches + ~/.gradle/wrapper, key ${{ runner.os }}-gradle-deploy-${{ github.sha }}, restore-keys …-gradle-deploy- then …-gradle-
  • Save Gradle cache guarded by always() && steps.gradle-cache.outputs.cache-hit != 'true'

Staging (push to main) and prod (dispatch from main) share cache scope, so the prod deploy gets an exact hit on the key the staging deploy saved. A service opts in with cache-gradle-build-outputs: true in its deploy workflows.

Measured effect

Throwaway branch in service-energy calling this branch's component-test-kotlin.yml (test job only, no deploy; Blacksmith 16 vCPU arm64, same config as the deploy). Test project step, same sources every time:

Run Situation Test project Tasks from cache
1 cold, nothing to restore 3m32s 0 (matches today's 3m31s baseline)
3 new SHA, restored previous deploy archive 1m17s 3 (compileKotlin, compileTestKotlin, :test)
4 same SHA re-pushed, exact key hit 2m33s 2 (compile only)
A new SHA, restored previous archive 2m40s 2 (compile only)
B same SHA as A re-pushed, exact key hit 1m20s 3 (compile and :test)
  • Compilation always came from cache once an archive existed. :test came from cache in 2 of 4 follow-up runs; in the other two its cache key changed between runs and I did not find out why.
  • :kspKotlin and :kspTestKotlin never came from cache (about 45 s plus 15 s); their keys differ on every run.
  • So a prod deploy of an already-staged SHA should land between roughly 1m20s and 2m35s, against 3m30s to 4m30s today.
  • Staging deploys of a new SHA only benefit for tasks whose inputs did not change (dependencies, and compile when only non-source files changed).

Considerations for reviewers

  • Cache cost (question for SRE): each deploy that misses the exact key saves a ~1.1 GB archive (about twice today's setup-java archive, since it now holds build-cache entries). Restore takes ~15 s, save ~9 s.
    • On GitHub-hosted or ARC runners using the GitHub cache, the archives count against the 10 GB per-repo limit (service-energy uses 2.4 GB today). About 9 staging deploys would fill it and evict the PR workflow's Gradle caches and the Sonar baseline.
    • On Blacksmith runners (service-energy's deploy test job) actions/cache goes through Blacksmith's cache, not GitHub's. I don't know how that storage is billed; please check.
    • Options: save only on pushes to main, or key per branch (loses the staging to prod exact hit).
  • Prod reuses staging's :test result when inputs are identical (normal Gradle semantics). Tests that depend on time or external state are not re-run at prod.
  • Only service-energy was measured. Other Kotlin services benefit only if their prod deploy also re-tests a staged SHA.
  • code-coverage-kotlin.yml and component-service-profile-kotlin.yml have been removed or changed on main; not touched here.

Ticket: https://montaapp.atlassian.net/browse/ENERGY-2742 (draft, CODEOWNERS is @monta-app/sre)

🤖 Generated with Claude Code

https://claude.ai/code/session_014AaR9VvceeKm4EFQLLHX3s

…e [ENERGY-2742]

The test job cached Gradle only through actions/setup-java's cache: 'gradle',
which keys on a hash of the dependency files and, on an exact primary-key hit,
does not save. The build-cache-1 inside that archive is therefore a frozen
snapshot that never accumulates task outputs, so every deploy re-runs KSP and
both Kotlin compiles from scratch.

service-energy production deploy 2026-09-06 (run 34027976026): 'Test project'
6m33s, 'BUILD SUCCESSFUL in 6m 31s', '11 actionable tasks: 11 executed',
nothing FROM-CACHE; :kspKotlin ~63s, :compileKotlin ~45s, :kspTestKotlin ~30s,
:compileTestKotlin ~30s. The pull-request workflow restores the same tasks
FROM-CACHE because it keys actions/cache on the commit and saves after a
non-exact restore.

Mirror that here: drop setup-java's cache input, restore with a per-commit key
plus '-gradle-deploy-' and '-gradle-' fallbacks, and save only when the restore
was not an exact hit.
@ckattmann

Copy link
Copy Markdown
Contributor Author

Update on the validation note: the repo's Lint GitHub Workflows check (actionlint) passed on this branch — https://github.com/monta-app/github-workflows/actions/runs/34052330363 — so the expression syntax and step references are lint-clean, covering the gap from not having actionlint installed locally. Still draft pending @monta-app/sre review.

@ckattmann ckattmann changed the title fix(component-test): let deploy test jobs reuse the Gradle build cache [ENERGY-2742] feat(component-test): opt-in Gradle build cache for deploy test jobs [ENERGY-2742] Oct 4, 2026
@ckattmann
ckattmann marked this pull request as ready for review October 4, 2026 13:39
@ckattmann
ckattmann requested a review from a team as a code owner October 4, 2026 13:39
@ckattmann
ckattmann requested review from tobias0106 and removed request for a team October 4, 2026 13:39
@joscdk

joscdk commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

@ckattmann regarding the cache storage, for ARC we are using a different cache package which stores on S3, could that be used for this?

@ckattmann

Copy link
Copy Markdown
Contributor Author

@ckattmann regarding the cache storage, for ARC we are using a different cache package which stores on S3, could that be used for this?

@joscdk If you mean runs-on/cache, yes that should be a drop-in replacement

@ckattmann
ckattmann requested a review from joscdk October 7, 2026 08:56
@ckattmann
ckattmann merged commit 17d99ba into main Oct 7, 2026
2 checks passed
@ckattmann
ckattmann deleted the fix/ENERGY-deploy-gradle-cache branch October 7, 2026 08:58
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.

2 participants