Caching
sbt 2.x caches task results across JVM runs (content-addressed, machine-wide; remote backends also share declared
outputs). That is why Aggregate stays cheap on a cold CI runner: zipx restores the cache before sbt test, keyed by a
commit-stable epoch (zipxCacheEpoch, default CacheEpoch.GitTags()). Every push within a PR reuses prior hits;
cutting a release tag rolls the epoch without regenerating ci.yml. Remote backends make the same story stronger
across machines, including developer laptops when CI hydrates a shared store (see Remote cache for teams).
This pairs with sbt-dynver-ci.
Miss path (amber → red) pays compile/test and writes digests; hit path (green) reuses them. Both land in the same
backend: LocalDir via actions/cache, or a remote gRPC store. The restore key is the commit-stable epoch
(zipxCacheEpoch), so PR pushes share hits and a release tag rolls a fresh namespace.
zipx wires cache into generated jobs (same planner as topology). It is not a standalone acceleration appliance: the goal is CI-from-graph plus content-addressed reuse, not a second product to configure beside hand-maintained YAML.
Epoch strategies
zipxCacheEpoch := CacheEpoch.GitTags() // default: resolve from git tags on the runner
zipxCacheEpoch := CacheEpoch.GitTags(tagMatch = "v*") // same, explicit match glob
zipxCacheEpoch := CacheEpoch.Fixed(version.value) // bake at generate time (old behaviour)
zipxCacheEpoch := CacheEpoch.Script(myEpochShell) // custom shell; must write epoch= and release=
GitTags (default): a Resolve cache epoch step runs after checkout (fetch-depth: 0, fetch-tags: true). On a
v* tag ref, epoch = release = tag without v. Otherwise the latest matching tag becomes release and epoch is
${release}-ci. If local tags lag origin (or none match), the step emits an Actions ::warning titled
zipx cache epoch so shallow/missing tags are obvious in the run summary.
Fixed: embeds a literal into the workflow at zipxWorkflowGenerate (useful for scripted tests or unusual versioning).
Prefer GitTags so post-tag PRs warm from the release cache without a regenerate commit.
Script: supply your own shell; write epoch= and release= to $GITHUB_OUTPUT. Restore-keys use both outputs.
Backends
zipxCache := CacheBackend.LocalDir
zipxCache := CacheBackend.BazelRemoteSidecar(RemoteCacheProof.image, RemoteCacheProof.port)
zipxCache := CacheBackend.ManagedRemote("grpcs://cache.buildbuddy.io", "BUILDBUDDY_KEY")
LocalDir: persist local cache dirs and
target/withactions/cache. Primary key is OS + JDK + epoch + run id + job id; restore-keys prefer the same run, then the epoch, then the prior release epoch (Fixed: strip-ci/-SNAPSHOT; GitTags/Script:steps.*.outputs.release) so the first post-tag PR can warm from the tag build, then any older OS+JDK sbt cache. No infrastructure. GitHub scopes cache entries to the branch that saved them; other PRs restore from the default branch. WithzipxSkipMergedPrPush, Verify does not run on the merge push, so by default a minimalcache-rehydratejob recreates a main-scoped save (see Verify). You cannot copy a PR cache onto main via the API.BazelRemoteSidecar: pinned
buchgr/bazel-remote-cacheas a job service; shared across the run via Bazel gRPC. Proof pins live inRemoteCacheProof(docs, planner tests, andRemoteCacheItSpecshare them).ManagedRemote: point sbt at BuildBuddy / EngFlow / NativeLink; auth header from a named repository secret. This is the path for CI-hydrated caches that developers reuse (see Remote cache for teams).
The remote-cache transport is bundled with zipx. For remote backends zipx also sets Global / cacheVersion from
(JDK, OS) so heterogeneous runners cannot poison the shared cache. Remote backends omit actions/cache (the gRPC
store is the persistence); LocalDir uses epoch-keyed actions/cache instead.
{
val local = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = CacheBackend.LocalDir),
)
val sidecar = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = RemoteCacheProof.sidecar),
)
val remote = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = CacheBackend.ManagedRemote("grpcs://cache.example", "CACHE_KEY")),
)
local + "\n---\n" + sidecar + "\n---\n" + remote
}test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
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
with:
disk-cache: "false"
- name: Cache sbt
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: |
~/.sbt
~/.cache/sbt
~/.cache/coursier
target
key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-test
restore-keys: |
ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
ubuntu-latest-jdk21-sbt-0.1.0-ci-
ubuntu-latest-jdk21-sbt-0.1.0-
ubuntu-latest-jdk21-sbt-
- 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'
---
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'Action pins
Generated workflows use commit-SHA pins (not floating @v4 tags), with # vX.Y.Z comments for readability.
Prefer .github/zipx/action-pins.yml (Dependabot-friendly) over pasting SHAs into build.sbt. Full guide:
Action pins (resolve order, Dependabot, zipxActionsPull, sync workflow, jar defaults).