Caching
zipx restores sbt's cache on the CI runner so the test job does not start from zero every time. You can ignore the knobs on this page until CI feels slow.
sbt 2 caches task results across JVM runs. zipx restores that cache before the Verify test task, 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 share the same hits 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 inside zipx-sbt-setup, 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.ShipCatalog // LocalDir namespace from Ship / ShipGroup rows
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.
ShipCatalog: for independent outbound versions (Ship / ShipGroup; see Independent versions). Bakes a SHA-256
of sorted Ship identity and version as the setup composite cache-epoch input (same generate-time path as Fixed).
One row bump rolls the repo-wide LocalDir key, the same way a v* tag does under GitTags(). Recommended when
ships are present. Lockstep OSS keeps GitTags(). examples/monorepo sets ShipCatalog.
{
val ships = List[PublishedRow](Ship("client", "0.3.0"), ShipGroup("libs", "1.4.2")("models", "coreLib"))
val hash = Modver.epochHash(ships)
val yaml = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cacheEpoch = CacheEpoch.ShipCatalog, shipEpochHash = Some(hash)),
)
s"hash: $hash\n$yaml"
}hash: 8f2e77a00c7c0f58
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
local-cache: "true"
cache-epoch: "8f2e77a00c7c0f58"
- name: test
run: sbt 'test'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/cacheinside the generatedzipx-sbt-setupcomposite. 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. CacheEpoch.ShipCatalog does not fold the Ship
hash into that value. A bump already changes that module's version, which is a digest input, so only that module's
remote entries miss. Remote backends turn off LocalDir cache in zipx-sbt-setup (the gRPC store is the persistence);
LocalDir passes local-cache: true so the composite runs epoch-keyed actions/cache.
{
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "true"
local-cache: "false"
cache-epoch: "0.1.0-ci"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "true"
local-cache: "false"
cache-epoch: "0.1.0-ci"
- name: test
run: sbt 'test'Action pins
Generated workflows use commit-SHA pins (not floating @v4 tags), with # vX.Y.Z comments for readability.
Catalog Action vals overlay jar defaults. Full guide: Action pins (overlay, zipxActionUpdate, leftover YAML,
jar defaults from the last zipx compile).