skip to content

On CI you have both PR builds and a main-branch build. How should each participate in the remote cache, and how do you express that split so PR builds don't degrade the shared cache?

level: middleimportance: should knowfreq 30%

answer

  1. main = sole seeder
  2. PR builds pull-only
  3. reuse density vs eviction
  4. -PcacheSeed=true in pipeline
  5. don't branch-sniff in build code

basics

~20 s

Make the main-branch (or release) job the only pusher and let PR builds pull read-only. Drive it from an invocation flag/property the seed job passes, e.g. isPush = isMainBranch, while every job keeps isEnabled = true to read.

solid answer

~40 s

Even on CI, not every job should push. PR builds run churny, short-lived code; if they push, they flood the shared cache with entries almost nobody reuses, evicting the valuable main-branch entries and hurting hit rate. So treat the **main/release job as the seed** (the only pusher) and let PR builds **pull read-only**. Express it with a flag the seed job passes rather than branch-sniffing in build code: ```kotlin buildCache { remote<HttpBuildCache> { isEnabled = true // all CI jobs read isPush = startParameter.projectProperties["cacheSeed"] == "true" } } ``` The main-branch pipeline invokes `./gradlew build -PcacheSeed=true`; PR pipelines omit it. PR builds still get fast pulls from main's seed, but contribute nothing, keeping the cache stable and high-value. This is the CI-internal refinement of the broader CI-pushes/devs-pull split.

code

bash · 5 lines
bash
# main-branch pipeline step (the seed job)
./gradlew build -PcacheSeed=true

# pull-request pipeline step (pull-only)
./gradlew build

go deeper

for a junior

Knows CI pushes and devs pull; may not distinguish PR vs main.

for a middle

Restricts push to the main/seed job and keeps PR builds pull-only via a flag.

for a senior

Reasons about reuse density, eviction, and keeping branch logic out of build code.

for a principal

Sets the org seeding strategy (which jobs seed, eviction/quota policy) and the invocation contract across pipelines.

## The problem with 'all CI pushes' The simple rule is "CI pushes, developers pull." But CI itself has heterogeneous jobs: - **Main/release builds** build stable, long-lived code that everyone branches from. Their outputs are exactly what other builds will want — high reuse. - **PR/feature builds** build transient code. Their task outputs are rarely reused by anyone else, because the PR will merge or close soon. If they push, they fill the cache with low-value entries that can **evict** the valuable main entries (caches have finite size and eviction policies), lowering the overall hit rate. ## The refinement: seed from main only Let the **main-branch (or tagged release) job be the sole seeder** — `isPush = true` there — and have **every** CI job (PR and main) keep `isEnabled = true` so they all benefit from pulls. PR builds get fast incremental hits from main's seed without polluting it. ## Express it via invocation, not branch-detection in build code Keep build logic environment-agnostic and let the CI pipeline express intent through a property or task: ```kotlin // settings.gradle.kts buildCache { remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isEnabled = true isPush = startParameter.projectProperties["cacheSeed"] == "true" } } ``` Pipeline side: ```bash # main-branch job ./gradlew build -PcacheSeed=true # pull-request job ./gradlew build # no flag → pull-only ``` This keeps the decision in the pipeline definition (where branch context naturally lives) rather than hard-coding `if (branch == 'main')` inside Gradle, which is brittle and couples the build to CI internals. ## Why not push from PRs 'just in case'? Because cache value is about **reuse density**. Entries that only one ephemeral build will ever read are pure cost: storage, eviction pressure, and noise in hit diagnostics. Concentrating writes on the stable seed maximizes the ratio of useful entries. ## Combined picture - Developers: `isEnabled = true`, `isPush = false` (read-only). - PR CI: `isEnabled = true`, `isPush = false`. - Main/release CI (the seed): `isEnabled = true`, `isPush = true`. All three pull; only the seed writes. That is the complete push/pull-and-seeding strategy for a shared cache.

  • Why can PR builds pushing actually lower the overall cache hit rate?
    Caches are finite and evict. PR entries are rarely reused but consume space, evicting valuable main-branch entries that many builds need, so hit rate drops.
  • Why pass a -P flag instead of writing `if (branch == 'main')` in the build script?
    Branch context belongs in the CI pipeline. Keeping the build environment-agnostic and gating on an explicit flag avoids coupling Gradle logic to CI internals and is easier to test.

saying these in an interview costs you the question

  • Letting every CI job push 'to maximize cache fill' — degrades reuse density.
  • Hard-coding branch names inside settings.gradle to decide pushing.

context