Remote cache for teams
CI should make local builds faster, not only green checks. On sbt 2.x, task and test results are content-addressed
and remote-ready. When zipx CI runs sbt test (or publish) against a shared remote cache, it hydrates digests for
the commits your team already built. The next laptop (or the next PR job) downloads those results instead of
recompiling and retesting the same bytecode.
That reverses a familiar monorepo bruise: every morning you pull a dozen teammates' changes and spend the first hour building their code. With a CI-hydrated cache, most of that work is already a hit. See sbt's caching overview for the pure-function mental model; here is the zipx / team angle.
Remote cache is necessary but not the whole recovery. Topology (jobs, needs, gates) still comes from the sbt graph.
A cache product alone can leave disconnected CI as a second source of truth (see Why zipx). We want both: shared
digests and an honest graph.
What we claim / what we do not
Honest boundaries help you trust the path:
| Claim | Status |
|---|---|
| Content-addressed reuse of compile/test across machines | Yes (sbt 2 + Bazel-compat gRPC) |
| CI hydrates; developers and later jobs Get | Yes (ManagedRemote / long-lived sidecar) |
JDK/OS partitioned via cacheVersion | Yes (zipx wiring) |
| Sandboxed hermetic builds / remote execution | No (use Graph for job fan-out) |
Live Put/Get is proven by RemoteCacheItSpec (Testcontainers + the same RemoteCacheProof pins as the YAML below).
What gets shared
sbt 2 caches task results (and, for remote backends, declared file outputs). Incremental compile and test
already invalidate at fine grain:
Compile follows Zinc's class/API graph inside a subproject.
Test uses hermetic suite digests over transitive bytecode (not timestamps), so a successful suite can be skipped across machines when inputs match.
You do not need to explode the repo into Bazel-style package-level targets to benefit. In Bazel, the cache unit is
often the BUILD target; many small targets improve hit rates because that is the boundary. In sbt 2, the boundary
for compile/test is already class/suite digests inside your existing modules. Aggregate CI plus a remote cache is
enough for most libraries and multi-service monorepos. Escalate to Graph when you need job-level fan-out or
multi-environment isolation (see Execution modes), not merely to "make caching work."
CI hydrates; developers pull
Typical loop with a ManagedRemote (or long-lived sidecar) backend:
CI is the hydrator (amber): misses compile onsite, then Puts digests. Later PR jobs and laptops (green) Get
the same entries when JDK/OS cacheVersion and digests match.
A PR or
mainjob runs Aggregatesbt testwithZIPX_REMOTE_CACHEset.Misses compile/test onsite; successes write action-cache entries and outputs to the remote store.
Teammates (and later CI jobs) with the same JDK/OS
cacheVersionand matching digests Get those entries.Local
sbt testaftergit pullshows high cache %: cold JVM, warm digests.
LocalDir (default) already helps within GitHub Actions via epoch-keyed actions/cache. It does not share across
developer laptops. Remote backends are how the win leaves the datacenter.
zipx folds (JDK, OS) into Global / cacheVersion for remote backends so a heterogeneous runner pool cannot poison
the store. Point laptops at the same endpoint (and credentials) when you want local hits from CI hydration.
Cache is not remote execution
Bazel's Remote Execution API covers both cache and execute. sbt 2 (and zipx) use the cache side today: reuse completed work; when cold, the machine that scheduled the task still runs it.
| Remote cache | Remote execution | |
|---|---|---|
| Question | Has this digest been done? | Run this action on a worker pool |
| sbt 2 / zipx | Yes (Bazel-compat gRPC) | Not pursued; use Graph for job fan-out |
| Team win | CI hydrates; everyone skips redo | Wall-clock via many workers |
If wall clock is still bound by many independent misses on a huge PR, prefer Graph (more runners, path-based affected jobs) plus remote cache, rather than rewriting the build as package targets. True task-level remote execution would need hermetic action workers inside sbt; that is build-tool work, not a zipx toggle.
Turn it on
Backends and generated YAML live on the Caching page. The short version:
zipxCache := CacheBackend.ManagedRemote("grpcs://cache.example", "CACHE_KEY")
// or proof-pinned sidecar:
zipxCache := RemoteCacheProof.sidecar
Then regenerate the workflow, add the repository secret, and confirm CI exports ZIPX_REMOTE_CACHE. For a
zero-infra start, keep LocalDir and graduate to ManagedRemote when the team wants laptop reuse of CI digests.
{
val managed = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = CacheBackend.ManagedRemote("grpcs://cache.example", "CACHE_KEY")),
)
val sidecar = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = RemoteCacheProof.sidecar),
)
managed + "\n---\n" + sidecar
}test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
env:
ZIPX_REMOTE_CACHE: grpcs://cache.example
ZIPX_REMOTE_CACHE_HEADER: ${{ secrets.CACHE_KEY }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: Setup JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
with:
distribution: temurin
java-version: "21"
- uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
- name: test
run: sbt 'test'
---
test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
services:
bazel-remote:
image: buchgr/bazel-remote-cache:v2.6.1
ports:
- "9092:9092"
options: --max_size=1
env:
ZIPX_REMOTE_CACHE: grpc://localhost:9092
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: Setup JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
with:
distribution: temurin
java-version: "21"
- uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
- name: test
run: sbt 'test'