kache 0.28.0

Zero-copy, content-addressed build cache for Rust, C/C++ and more, with S3 and shared-filesystem remotes.
---
title: CI setup
description: Use Kache in GitHub Actions or a shell-based CI job
---

## GitHub Actions

The official action installs Kache, configures the Rust wrapper, and can persist the local store through GitHub's cache service:

```yaml title=".github/workflows/ci.yml"
steps:
  - uses: actions/checkout@v4
  - uses: dtolnay/rust-toolchain@stable
  - uses: kunobi-ninja/kache-action@v1
  - run: cargo build --locked
```

See the [kache-action repository](https://github.com/kunobi-ninja/kache-action) for its current input reference and release notes.

For S3-compatible storage:

```yaml
- uses: kunobi-ninja/kache-action@v1
  with:
    s3-bucket: my-build-cache
    s3-region: eu-west-1
    s3-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }}
    s3-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }}
```

Add `s3-endpoint` for R2, Ceph, or MinIO. Prefer workload identity or IAM roles over long-lived access keys when the runner supports them.

## Who may write

A write credential for an S3 or filesystem remote can overwrite or delete objects.

On GitHub Actions and GitLab CI, Kache suppresses remote writes unless the job is a **push to a protected branch**. Pull requests, merge requests, tags, unprotected branches, `workflow_dispatch`, and scheduled runs are read-only even if the job has a write credential. `KACHE_REMOTE_READONLY=0` does not turn that off.

A laptop or other non-CI process is left as configured, so `kache sync --push` still works locally. Set `remote_readonly = true` under `[cache]` there when you want reads without writes. `KACHE_REMOTE_READONLY=1` affects only the process that reads it: a daemon that a build starts drops it.

```yaml
# Optional: make the read-only intent explicit in pull-request jobs.
env:
  KACHE_REMOTE_READONLY: "1"
```

GitHub withholds secrets and OIDC tokens from fork pull-request jobs, so those runs cannot publish unless you also pass a write credential another way. Same-repository pull requests do receive secrets; the CI policy above is what stops them writing.

Pull-request jobs can still restore entries a trusted branch already published.

### A prefix for pull requests

Read-only pull requests never reuse their own work: each push recompiles what the pull request changed, even when the previous push built it. Give pull requests a prefix of their own and they can:

```toml
[cache.remote]
type = "s3"
bucket = "acme-build-cache"
prefix = "artifacts"
pull_request_prefix = "artifacts-pr"
```

`KACHE_PULL_REQUEST_PREFIX` sets the same value. In a pull request job (a GitHub `pull_request` event or a GitLab merge request pipeline), Kache then:

- reads the base prefix first and the pull request prefix for what the base lacks;
- writes entries, build manifests, shards and prediction rows only under the pull request prefix.

Every other job ignores the setting: protected-branch pushes never read the pull request prefix, and tags, unprotected branches and `pull_request_target` jobs stay read-only. The pull request prefix must differ from `prefix`, and neither may contain the other; otherwise the job stays read-only and Kache logs why.

A pull request job with a prefix runs its own daemon. Its socket and upload queue carry a name derived from the prefix (`daemon-pr-<hash>.sock`, `upload-queue-pr-<hash>`), so it never hands uploads to a protected-branch job's daemon or reads through one. It still must not share a local store with trusted jobs: an artifact it compiles there would be a local hit for the next trusted build. Keep pull request jobs off the persistent disk described in [Ephemeral and persistent runners](#ephemeral-and-persistent-runners).

Kache's policy is not the boundary; the bucket policy is. Give the role that pull request jobs assume read access to both prefixes and write access to the pull request prefix only:

```json
{
  "Statement": [
    { "Effect": "Allow", "Action": "s3:GetObject",
      "Resource": ["arn:aws:s3:::acme-build-cache/artifacts/*",
                   "arn:aws:s3:::acme-build-cache/artifacts-pr/*"] },
    { "Effect": "Allow", "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::acme-build-cache/artifacts-pr/*" }
  ]
}
```

Expire the pull request prefix with a lifecycle rule. One prefix is shared by all pull requests of the repository, so someone who can open a pull request can affect another pull request's builds, but never `main`, a tag or a release.

In a `container:` job, the runner mounts `HOME` and `runner.temp` separately from the workspace, and a hardlink cannot cross a mount. Leave `cache-dir` unset and the action puts the cache on the workspace's mount. A `cache-dir` on another mount still works, but kache copies every artifact into `target/`.

C and C++ objects publish through the same remote as Rust. A daemon-backed compile restores them on miss; `kache sync --pull --all` also warms them in the local store. clang-cl debug objects stay on the machine and are not published.

## Other CI systems

Install Kache and write the remote to its config file before the first build. The background daemon uploads what the build compiles. A daemon that a build starts drops `KACHE_S3_BUCKET`, `KACHE_S3_REGION`, `KACHE_S3_ENDPOINT` and the other [remote settings](/docs/daemon/lifecycle#updates-and-config-changes) from its environment, so the remote has to be in the file. CI has no terminal to answer prompts, so pass `--yes`:

```bash
mkdir -p ~/.config/kache
printf '[cache.remote]\ntype = "s3"\nbucket = "my-build-cache"\nregion = "eu-west-1"\n' > ~/.config/kache/config.toml

kache init --yes
cargo build --locked
kache daemon stop
```

`~/.config/kache/config.toml` is the default path. If the job sets `KACHE_CONFIG` or `XDG_CONFIG_HOME`, write the file there instead.

`kache daemon stop` lets the daemon finish its queued uploads before it exits, so run it before the runner goes away. `kache sync --push` does not replace it: inside a Cargo workspace it uploads only the workspace's own crates, without their dependencies. The daemon keeps credentials from the environment, so they can stay in `KACHE_S3_ACCESS_KEY` and `KACHE_S3_SECRET_KEY`, the runner's secret store, or the standard cloud identity chain. Use `--allow-partial` only when cache failure must not fail the job.

## Ephemeral and persistent runners

Speculative prefetch is intended for empty runners. A persistent self-hosted runner may already have the needed entries locally. Measure both modes and disable speculation when it adds work:

```toml
[cache]
prefetch_enabled = false
```

This keeps exact remote reads and uploads.

Concurrent jobs may share a persistent local store, but each job should have a distinct `KACHE_RUNTIME_DIR` for sockets, locks, event logs, and session state. kache-action already gives each job its own runtime directory. For a trusted disk that persists across jobs, set `node-cache: true` and `cache-dir` on the action.

On a trusted builder that keeps a disk across jobs, point `KACHE_CACHE_DIR` at that disk and set `KACHE_TRUST_DOMAIN` to one label for the trust domain (for example `github.kunobi-ninja.kache`). Kache stores under `$KACHE_CACHE_DIR/<label>/`. Keep `KACHE_RUNTIME_DIR` on the job's scratch disk so sockets die with the pod. Do not mount that disk or export `KACHE_TRUST_DOMAIN` on public or fork jobs. An unusable label leaves `KACHE_CACHE_DIR` unchanged.

## Progress and reports

Kache is quiet in compiler-wrapper mode by default because Cargo keeps a compiler's stderr and replays it when the unit is fresh. `KACHE_PROGRESS` turns on progress lines. From a terminal they go straight to it, out of Cargo's reach. In CI there is no terminal, so they go to stderr, and a later Cargo command in the same job can replay a line for a unit Kache did not run again.

```yaml
env:
  KACHE_PROGRESS: hits       # hits only
  # KACHE_PROGRESS: verbose # hits, dups, misses, and long-compile heartbeats
```

Create a machine-readable summary after the build:

```bash
kache report --format json --output kache-report.json
kache stats
```

## Coverage and uncached steps

Coverage instrumentation uses separate, path-local keys. If you do not want coverage artifacts in Kache:

```yaml
- run: cargo llvm-cov
  env:
    KACHE_DISABLED: "1"
```

Unset `KACHE_DISABLED` to enable caching again. Only `1` and `true` disable it.