Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7ab2f5c
Add sticky disk cache support via new cache/path inputs
crohr Jul 3, 2026
83d08fd
Document cache/path inputs
crohr Jul 3, 2026
b43f07b
Add buildkit cache mode (dedicated buildkitd on sticky disk)
crohr Jul 4, 2026
e14ca55
Add disk-pressure handling for sticky disk caches
crohr Jul 4, 2026
a1900ef
Add Windows support to sticky disk cache modes
crohr Jul 4, 2026
18872e7
Rebuild action binaries
crohr Jul 4, 2026
782b1f3
Add git cache mode (fast checkouts via sticky-disk mirrors)
crohr Jul 8, 2026
4505b1c
Rebuild action binaries
crohr Jul 8, 2026
51a0a92
Update snap label documentation
crohr Jul 10, 2026
0f9527f
Delegate sticky BuildKit to setup-buildx (#45)
crohr Jul 24, 2026
0246065
Merge remote-tracking branch 'origin/main' into auto/review-pr-43
crohr Jul 24, 2026
9285517
Address sticky cache review feedback
crohr Jul 24, 2026
8353fcb
dist: rebuild binaries
crohr Jul 24, 2026
c9c58fe
Address remaining sticky cache reviews
crohr Jul 24, 2026
c09594e
dist: rebuild binaries
crohr Jul 24, 2026
9739fe9
ci: use bootstrap RunsOn environment
crohr Jul 24, 2026
b40a240
Harden sticky cache lifecycle
crohr Jul 24, 2026
655b799
dist: rebuild binaries
crohr Jul 24, 2026
cfac959
ci: test runs-on cache v5
crohr Jul 24, 2026
e4747e7
test: wait for sticky snapshots to settle
crohr Jul 24, 2026
687e271
fix: clean up failed sticky cache setup
crohr Jul 24, 2026
fd8bda5
fix: harden interrupted sticky caches
crohr Jul 24, 2026
6d53342
fix: close sticky cache lifecycle gaps
crohr Jul 24, 2026
6accd3e
fix: close final sticky cache review gaps
crohr Jul 24, 2026
ab459ae
fix: preserve fallback sticky cache state
crohr Jul 24, 2026
7363fae
fix: ignore undeclared legacy inputs
crohr Jul 24, 2026
438b50e
fix: recover interrupted sticky cache state
crohr Jul 24, 2026
9f9fb83
ci: bound missing sticky disk wait
crohr Jul 24, 2026
bdd8fc7
fix: validate restored sticky cache state
crohr Jul 24, 2026
0d3160a
fix: recover interrupted BuildKit volume setup
crohr Jul 25, 2026
cd12007
fix: harden sticky cache recovery paths
crohr Jul 25, 2026
5931d81
fix: secure sticky cache reuse edge cases
crohr Jul 25, 2026
a26376f
fix: close sticky cache authorization gaps
crohr Jul 25, 2026
ed4191e
fix: close remaining sticky cache edge cases
crohr Jul 25, 2026
d7fcf9c
fix: track cross-step sticky cache state
crohr Jul 25, 2026
bfebbeb
fix: restore sticky cache host state
crohr Jul 25, 2026
9a322ad
fix: make sticky cleanup failures safe
crohr Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/test-container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
image: [ubuntu24-full-x64, ubuntu24-full-arm64]
container:
- ubuntu:24.04
runs-on: runs-on=${{ github.run_id }}/cpu=2/family=m7/image=${{ matrix.image }}/extras=s3-cache
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=${{ matrix.image }}/extras=s3-cache
container: ${{ matrix.container }}
env:
ARTEFACT_NAME: ${{ github.run_id }}-${{ strategy.job-index }}-my-artifact
Expand Down Expand Up @@ -100,22 +100,22 @@ jobs:
restore-keys: |
github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-
- name: Save to cache (runs-on/cache)
uses: runs-on/cache/save@v4
uses: runs-on/cache/save@v5
with:
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-${{ env.FILENAME }}
- name: Restore from cache (runs-on/cache)
uses: runs-on/cache/restore@v4
uses: runs-on/cache/restore@v5
with:
fail-on-cache-miss: true
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-${{ env.FILENAME }}
- name: Restore from cache (runs-on/cache, restoreKeys)
uses: runs-on/cache/restore@v4
uses: runs-on/cache/restore@v5
with:
fail-on-cache-miss: true
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-unknown
restore-keys: |
github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-
# END RUNS-ON CACHE
# END RUNS-ON CACHE
4 changes: 2 additions & 2 deletions .github/workflows/test-default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu24-full-x64, ubuntu24-full-arm64, windows22-base-x64]
runs-on: runs-on=${{ github.run_id }}/cpu=2/family=m7/image=${{ matrix.image }}
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=${{ matrix.image }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: ./
- run: sleep 30
- run: sleep 30
10 changes: 5 additions & 5 deletions .github/workflows/test-magic-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
image: [ubuntu24-full-x64, ubuntu24-full-arm64, windows22-base-x64]
runs-on: runs-on=${{ github.run_id }}/cpu=2/family=m7/image=${{ matrix.image }}/extras=s3-cache
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=${{ matrix.image }}/extras=s3-cache
env:
ARTEFACT_NAME: ${{ github.run_id }}-${{ strategy.job-index }}-my-artifact
FILENAME: random-file
Expand Down Expand Up @@ -102,22 +102,22 @@ jobs:
restore-keys: |
github-${{github.run_id}}-actions-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-
- name: Save to cache (runs-on/cache)
uses: runs-on/cache/save@v4
uses: runs-on/cache/save@v5
with:
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-${{ env.FILENAME }}
- name: Restore from cache (runs-on/cache)
uses: runs-on/cache/restore@v4
uses: runs-on/cache/restore@v5
with:
fail-on-cache-miss: true
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-${{ env.FILENAME }}
- name: Restore from cache (runs-on/cache, restoreKeys)
uses: runs-on/cache/restore@v4
uses: runs-on/cache/restore@v5
with:
fail-on-cache-miss: true
path: ${{ env.FILENAME }}
key: github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-unknown
restore-keys: |
github-${{github.run_id}}-runs-on-cache-${{strategy.job-index}}-${{ env.BLOCKS }}MiB-
# END RUNS-ON CACHE
# END RUNS-ON CACHE
4 changes: 2 additions & 2 deletions .github/workflows/test-metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
matrix:
image: [ubuntu24-full-x64, ubuntu24-full-arm64]
configuration: ["existing-aws-creds", "no-aws-creds", "cw-agent-stopped"]
runs-on: runs-on=${{ github.run_id }}/cpu=2/family=m7/image=${{ matrix.image }}
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=${{ matrix.image }}
steps:
- uses: actions/checkout@v6
- name: configure aws credentials
Expand Down Expand Up @@ -56,4 +56,4 @@ jobs:
sleep 10
done &

wait
wait
4 changes: 2 additions & 2 deletions .github/workflows/test-sccache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:

jobs:
test-sccache:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/runner=2cpu-linux-x64/extras=s3-cache

steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -103,4 +103,4 @@ jobs:
echo "SCCACHE_BUCKET: $SCCACHE_BUCKET"
echo "SCCACHE_REGION: $SCCACHE_REGION"
echo "SCCACHE_S3_KEY_PREFIX: $SCCACHE_S3_KEY_PREFIX"
echo "RUSTC_WRAPPER: $RUSTC_WRAPPER"
echo "RUSTC_WRAPPER: $RUSTC_WRAPPER"
117 changes: 117 additions & 0 deletions .github/workflows/test-sticky-buildkit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Test / Sticky BuildKit

on:
push:
branches:
- main
- v*
- feature/*
- fix/*
- auto/*
workflow_dispatch:

jobs:
cold:
if: github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- id: pinned
version: v0.34.1
- id: latest
version: latest
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=ubuntu24-full-x64/sticky=buildkit-${{ github.run_id }}-${{ matrix.id }}:20gb
steps:
- uses: actions/checkout@v6
- id: runs-on
uses: ./
with:
sticky_cache: |
buildkit
custom,path=.runs-on-custom-cache/${{ matrix.id }}
- name: Populate custom cache
run: echo "${{ matrix.id }}" > .runs-on-custom-cache/${{ matrix.id }}/marker
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
name: ${{ steps.runs-on.outputs.buildkit-builder }}
version: ${{ matrix.version }}
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.31.1
buildkitd-config-inline: ${{ steps.runs-on.outputs.buildkit-inline-config }}
cleanup: false
- name: Populate BuildKit cache
shell: bash
run: |
mkdir -p /tmp/runs-on-buildkit-test
printf 'FROM busybox:1.36\nRUN dd if=/dev/zero of=/layer.bin bs=1M count=16\n' > /tmp/runs-on-buildkit-test/Dockerfile
docker buildx build --builder "${{ steps.runs-on.outputs.buildkit-builder }}" --progress=plain --load --tag runs-on-buildkit-test:cold /tmp/runs-on-buildkit-test

wait-for-snapshot:
needs: cold
runs-on: ubuntu-slim
steps:
- name: Wait for the cold snapshot to become selectable
# Snapshot creation starts asynchronously after the cold runner exits.
# Leave enough time for both finalization and EC2 snapshot selection.
run: sleep 120

warm:
needs: wait-for-snapshot
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- id: pinned
version: v0.34.1
- id: latest
version: latest
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=ubuntu24-full-x64/sticky=buildkit-${{ github.run_id }}-${{ matrix.id }}:20gb
steps:
- uses: actions/checkout@v6
- id: runs-on
uses: ./
with:
sticky_cache: |
buildkit
custom,path=.runs-on-custom-cache/${{ matrix.id }}
- name: Verify restored state
env:
CACHE_HIT: ${{ steps.runs-on.outputs.cache-hit }}
run: |
test "$CACHE_HIT" = true
test "$(cat .runs-on-custom-cache/${{ matrix.id }}/marker)" = "${{ matrix.id }}"
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
name: ${{ steps.runs-on.outputs.buildkit-builder }}
version: ${{ matrix.version }}
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.31.1
buildkitd-config-inline: ${{ steps.runs-on.outputs.buildkit-inline-config }}
cleanup: false
- name: Verify cached build
shell: bash
run: |
mkdir -p /tmp/runs-on-buildkit-test
printf 'FROM busybox:1.36\nRUN dd if=/dev/zero of=/layer.bin bs=1M count=16\n' > /tmp/runs-on-buildkit-test/Dockerfile
docker buildx build --builder "${{ steps.runs-on.outputs.buildkit-builder }}" --progress=plain --load --tag runs-on-buildkit-test:warm /tmp/runs-on-buildkit-test 2>&1 | tee /tmp/runs-on-buildkit.log
grep -q 'CACHED' /tmp/runs-on-buildkit.log

missing-sticky-disk:
runs-on: runs-on=${{ github.run_id }}/env=bootstrap/cpu=2/family=m7/image=ubuntu24-full-x64
steps:
- uses: actions/checkout@v6
- id: sticky_cache
continue-on-error: true
uses: ./
with:
sticky_cache: buildkit
sticky_wait_timeout: 1s
- name: Verify missing sticky disk failed
env:
OUTCOME: ${{ steps.sticky_cache.outcome }}
run: test "$OUTCOME" = failure
141 changes: 141 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,147 @@ echo "SCCACHE_S3_KEY_PREFIX=cache/sccache" >> $GITHUB_ENV
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
```

### `sticky_cache`

Available for Linux and Windows runners on jobs with a sticky-disk label. Use `sticky=<size>` for the default snapshot lineage or `sticky=<name>:<size>` for a named lineage; the optional name must come first. Volume settings follow the size, for example `sticky=go-cache:20gb:gp3:750mbs:6000iops`. The `apt`, `buildkit`, and `git` cache modes are Linux only.

Persists package manager caches across jobs by bind-mounting them onto the job's sticky disk — a dedicated EBS volume that is snapshotted at job completion and restored (per repo, name, architecture, and branch) on the next job. No tarball upload/download: caches are available at native disk speed, with no size penalty on job duration.

Example:

```yaml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=20gb
steps:
- uses: actions/checkout@v4
- uses: runs-on/action@v2
with:
sticky_cache: |
go
node
```

Each non-empty line is one cache record. A record starts with a mode and may
include comma-separated `key=value` options. Use one line per mode; the old
comma-separated mode list is not supported.

```yaml
with:
sticky_cache: |
go
node
buildkit
custom,path=vendor/custom-cache,path=~/.cache/my-tool
```

The action fails if the sticky disk is absent or does not become ready before
`sticky_wait_timeout`. The `custom` mode requires one or more `path=` options;
repeat the record or option to persist several paths. Relative paths resolve
from `GITHUB_WORKSPACE`, `~/` resolves from the runner home, and absolute paths
are preserved. Literal commas in paths are unsupported.

Supported cache modes and the directories they persist:

| Mode | Aliases | Cached paths |
|---|---|---|
| `go` | `golang` | `~/.cache/go-build`, `~/go/pkg/mod` |
| `node` | `npm` | `~/.npm` |
| `yarn` | | `~/.cache/yarn` |
| `pnpm` | | `~/.local/share/pnpm/store` (or `$XDG_DATA_HOME/pnpm/store`) |
| `ruby` | `bundler` | `~/.bundle`, `vendor/bundle` |
| `rust` | `cargo` | `~/.cargo/registry`, `~/.cargo/git` |
| `python` | `pip` | `~/.cache/pip` |
| `uv` | | `~/.cache/uv` |
| `poetry` | | `~/.cache/pypoetry` |
| `apt` | | `/var/cache/apt/archives` |
| `buildkit` | `buildx` | BuildKit layer cache (the official setup-buildx builder stores its state on the sticky disk) |
| `git` | `checkout` | Git repository mirrors (local git proxy serving github.com fetches from the sticky disk) |
| `gradle` | | `~/.gradle/caches`, `~/.gradle/wrapper` |
| `maven` | | `~/.m2/repository` |
| `playwright` | | `~/.cache/ms-playwright` |
| `custom` | | One or more paths supplied with `path=` |

#### `buildkit` mode (Docker layer cache)

The `buildkit` mode prepares the state volume used by Docker's official `setup-buildx-action`, backed by the sticky disk. RunsOn does not download or start its own BuildKit daemon. The actions must run in this order, with the fixed builder topology and cleanup settings shown below:

```yaml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=docker:20gb
steps:
- uses: actions/checkout@v4
- id: runs-on
uses: runs-on/action@v2
with:
sticky_cache: buildkit
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
name: ${{ steps.runs-on.outputs.buildkit-builder }}
version: v0.34.1
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.31.1
buildkitd-config-inline: ${{ steps.runs-on.outputs.buildkit-inline-config }}
cleanup: false
- uses: docker/build-push-action@v7
with:
builder: ${{ steps.runs-on.outputs.buildkit-builder }}
context: .
load: true
```

Sticky BuildKit caching supports one `docker-container` node named by the `buildkit-builder` output. The RunsOn post step verifies that setup-buildx mounted the expected sticky volume, then stops and removes the builder before the disk is snapshotted. A missing setup step, reversed action order, different builder name, appended node, or setup-buildx cleanup causes a clear failure instead of silently using ephemeral cache storage.

The action always emits `buildkit-builder` and `buildkit-inline-config`, even without a sticky disk. When the RunsOn `ecr-pull-through` extra transparently mirrors Docker Hub, the inline config contains the matching BuildKit registry mirror; otherwise it is empty. This also supports a regular non-sticky builder:

```yaml
- id: runs-on
uses: runs-on/action@v2
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
name: ${{ steps.runs-on.outputs.buildkit-builder }}
buildkitd-config-inline: ${{ steps.runs-on.outputs.buildkit-inline-config }}
```

Use `docker buildx build` (or `docker/build-push-action`) with the emitted builder; add `--load` when you need the built image in the local Docker daemon. `docker pull` and plain `docker build` do not use this cache.

#### `git` mode (fast checkouts)

The `git` mode accelerates `actions/checkout` — and any other `git fetch`/`git clone` of a github.com repository during the job — without changing your checkout step. It starts a local git proxy whose bare repository mirrors live on the sticky disk, and rewrites `https://github.com/` fetch URLs to it (global `url.insteadOf`). The mirror syncs once per repo per job (only new objects cross the network); the fetch itself, including shallow `fetch-depth: 1` packs, is served at disk speed by `git upload-pack` on the runner.

Unlike other modes, the `git` mode must run **before** `actions/checkout`:

```yaml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=20gb
steps:
- uses: runs-on/action@v2
with:
sticky_cache: git
- uses: actions/checkout@v4
```

To combine it with any workspace-relative cache, including `ruby` (`vendor/bundle`) or a relative `custom,path=...`, run the action twice: use `git` before checkout, then run the workspace-relative cache after checkout. The second invocation reuses the already-running proxy. Combining them in one invocation fails early so a workspace mount cannot interfere with checkout.

Notes and limitations:

* Mirror syncs authenticate with the `token` input (default: `${{ github.token }}`). Checking out **other private repositories** requires passing a PAT with access to them, since the rewritten URLs no longer match the credentials configured by `actions/checkout`.
* `git push` is pinned to upstream (`pushInsteadOf`) and never goes through the proxy; Git LFS and anything else the proxy cannot serve is transparently forwarded to github.com. If mirroring fails for any reason, fetches fall back to upstream — the mode never breaks a build.
* SSH remotes (`git@github.com:`) are not rewritten, and container jobs (`container:`) are not accelerated (the proxy listens on the host's loopback). GitHub Enterprise Server is not supported. Linux only.

Use `custom,path=...` records to persist additional directories. Relative paths are resolved against the workspace, so run this action **after** `actions/checkout` when caching workspace-relative directories (e.g. `custom,path=vendor/bundle`). Custom file caches are not supported.

**Disk pressure:** the buildkit cache uses BuildKit's default garbage collection. Other cache modes have no native GC: when the volume drops below 20% free space (or 10% free inodes), the post step emits a warning and a job summary with a per-cache breakdown — increase the `sticky=` label size to fix. If a volume is ever critically full at job start (<5% free space or inodes), all caches on it are automatically reset so the job runs cold instead of failing with "no space left on device", and the next snapshot starts clean.

Other related inputs:

* `sticky_wait_timeout` - how long to wait for the sticky disk to be ready, as a Go duration (default `5m`)

The action sets a `cache-hit` output: `true` when every requested path was restored from a previous snapshot. It also sets `buildkit-builder` to the stable builder name and `buildkit-inline-config` to the ECR mirror TOML described above.

## Development

Make your source code changes in a commit, then rebuild and commit the generated binaries and JS files:
Expand Down
Loading
Loading