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.

yesno

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/ with actions/cache inside the generated zipx-sbt-setup composite. 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. With zipxSkipMergedPrPush, Verify does not run on the merge push, so by default a minimal cache-rehydrate job recreates a main-scoped save (see Verify). You cannot copy a PR cache onto main via the API.

  • BazelRemoteSidecar: pinned buchgr/bazel-remote-cache as a job service; shared across the run via Bazel gRPC. Proof pins live in RemoteCacheProof (docs, planner tests, and RemoteCacheItSpec share 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).