Caching strategy in CI
Reuse build results across CI runs — backends vs. store/restore.
Locally, hammerkit caches automatically: a task whose src is unchanged is
skipped. The challenge in CI is that each run usually starts from a fresh
checkout on a fresh machine, so there is no local cache to hit. This guide
covers the two ways to carry results across CI runs and which to choose.
Keep the cache method at the default checksum in CI. modify-date compares
file timestamps, which git clone resets on every run — so a modify-date task
never hits the cache on a fresh runner. See caching.
Option A — a remote cache backend (recommended)
Point the built-in default cache at an S3-compatible bucket (or a container
registry — see caches). Hammerkit then
pulls a task's result before running it and pushes the result after — no
cache scripting in your pipeline at all.
caches:
default:
method: checksum
backend:
type: s3
bucket: my-build-cache
region: eu-central-1
tasks:
build:
image: node:24-alpine
src:
- src
generates:
- dist
cmds:
- npm run buildCredentials come from the standard AWS SDK chain (AWS_ACCESS_KEY_ID /
AWS_SECRET_ACCESS_KEY), so set those as CI secrets. Backend errors never fail the
build — an unreachable bucket just falls back to running the task. The same bucket
works for developers locally, so a result built on a laptop is reused in CI and
vice versa. See caches for the full backend reference
(MinIO, R2, GCS).
Sharing across checkouts, runners and agent sandboxes
Cache ids are computed from paths relative to the project root (the git root), so
the same commit produces the same ids wherever it is checked out — a CI runner at
/home/runner/work/app/app, an agent sandbox at /workspace/app and a laptop all
hit the same entries. Use container tasks for anything you want to share
between macOS and Linux machines: a local task includes the host OS in its identity.
Read-only runners
Only trusted runners should write to the shared cache. Give untrusted runners — coding-agent sandboxes, pull requests from forks — read-only credentials and run them in read-only mode, so they restore results but never push:
export HAMMERKIT_CACHE_READ_ONLY=1 # or pass --cache-read-only to run / up
hammerkit run testRead-only mode skips every backend push, including the built-in local default
cache. Results a read-only run builds are still reused by later runs in the same
checkout.
Splitting network from compute
Instead of letting every task talk to the bucket during the build, keep tasks on
the machine-local default cache and move entries explicitly with
cache pull / cache push. The build itself then does no
network I/O, and a workspace can be warmed before anyone runs anything:
hammerkit cache pull --remote shared # network only
hammerkit run # compute only
hammerkit cache push --remote shared # trusted runners onlyOption B — store / restore with the CI's own cache
If you'd rather use your CI provider's built-in caching, wrap the run with
store / restore: restore pulls the previous
results out of a directory before the build, store writes them back after, and
the CI caches that directory.
hammerkit restore cache # before the build
hammerkit build
hammerkit store cache # after the buildA GitHub Actions job wiring that to actions/cache:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: npm i -g hammerkit
- uses: actions/cache@v4
with:
path: cache
key: hammerkit-${{ hashFiles('**/package-lock.json') }}
restore-keys: hammerkit-
- run: hammerkit restore cache
- run: hammerkit build
- run: hammerkit store cacheWhich one?
| Remote backend | store / restore | |
|---|---|---|
| Pipeline wiring | None — automatic pull/push | You add restore/store steps + a cache step |
| Granularity | Per task | One directory for the whole run |
| Shared with local dev | Yes (same bucket) | No (CI cache only) |
| Needs a bucket or registry | Yes | No |
A remote backend is the lower-maintenance choice and is shared with local development. If you already rely on your CI's cache and don't want a bucket, store/restore is fine. You rarely need both — if a remote backend is configured, store/restore is redundant.