RunsOn Action for magic caching, and more. This action is required if you are using the magic caching feature of RunsOn (extras=s3-cache job label).
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
steps:
- uses: step-security/runs-on-action@v2
- other stepsShow all environment variables available to actions (used for debugging purposes).
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
steps:
- uses: step-security/runs-on-action@v2
with:
show_env: truePossible values:
true- Show all environment variablesfalse- Don't show environment variables (default)
Displays how much it cost to run that workflow job, and compares it with a similar GitHub-hosted runner.
RunsOn v3.4.0 and later. The RunsOn agent reports the cost itself, at the very end of the job, in the "Complete runner" step. This happens for every job, with or without this action, so show_costs only chooses how it is displayed. The estimate:
- covers EC2 (on-demand or spot), the root EBS volume, and any sticky disk, from when the instance starts (or the job starts, on a warm-pool instance) until the job ends;
- uses prices your RunsOn control plane already resolves, so the runner makes no pricing or EC2 API calls and needs no internet access.
Example output in the "Complete runner" step:
💰 Estimated cost: $0.0034 (GitHub-hosted: $0.0120)
| Metric | Value |
| ------------------------ | ------------------------------------------ |
| Instance type | c7a.large |
| Instance lifecycle | spot |
| Region | us-east-1 |
| Availability zone | us-east-1b |
| Platform | linux/x64, 2 vCPUs |
| Billed duration | 2m45s (boot, job, and 5s for shutdown) |
| Job duration | 1m44s |
| EC2 | $0.0014 |
| EBS root volume | $0.0008 |
| EBS sticky disk | $0.0012 |
| Total | $0.0034 |
| GitHub-hosted equivalent | $0.0120 (job duration rounded up to 2 min) |
| Savings | $0.0086 (71.7%) |
The GitHub-hosted equivalent uses GitHub's published per-minute price for the smallest runner with at least as many vCPUs. It bills only the job duration, rounded up to the next whole minute as GitHub does, since GitHub doesn't bill runner boot.
Earlier RunsOn versions. The action's post step computes the cost from https://ec2-pricing.runs-on.com, for both on-demand and spot pricing across all regions and availability zones. It covers EC2 only, up to the post step. When the cost API has no matching pricing data, cost reporting logs an informational message and skips the cost table and job summary. This includes unsupported regions and unavailable instance or zone prices. Other API and network failures still warn.
Possible values:
inline- Display costs in the log output (default)summary- Display costs in the log output and in the GitHub job summary- Any other value - Disables the feature
When step-security/runs-on-action is invoked more than once in the same job, only the first
invocation with cost reporting enabled calculates and displays the job cost.
Later invocations skip duplicate reporting automatically. An invocation with
cost reporting disabled does not prevent a later enabled invocation from
reporting.
Note: this is currently only available with a development release of RunsOn. This will be fully functional with v2.8.4+
Send additional metrics using CloudWatch agent.
Supported metrics:
| Metric Type | Available Metrics |
|---|---|
cpu |
usage_user, usage_system |
network |
bytes_recv, bytes_sent |
memory |
used_percent |
disk |
used_percent, inodes_used, free, total |
io |
io_time, reads, writes |
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
steps:
- uses: step-security/runs-on-action@v2
with:
metrics: cpu,network,memory,disk,ioPossible values:
cpu- CPU usage metrics (usage_user,usage_system)network- Network metrics (bytes_recv,bytes_sent)memory- Memory metrics (used_percent)disk- Disk metrics (used_percent,inodes_used,free,total)io- I/O metrics (io_time,reads,writes)- Comma-separated combinations (e.g.,
cpu,network,memory,disk,io) - Empty string - No additional metrics (default)
Disk metrics are published for each of /, /tmp, /var/lib/docker and /home/runner that is a mount point, with the InstanceId, path, fstype, device and VolumeId dimensions. VolumeId is the EBS volume behind that mount, so a sticky disk or snapshot volume mounted at /var/lib/docker reports its own volume. Mounts that aren't on an EBS volume have no VolumeId, for example tmpfs, overlay, or the md0 array RunsOn builds from local instance storage. Earlier versions published disk metrics without the device and VolumeId dimensions, so update dashboards or alarms that match on the previous set.
The action will display live metrics with charts in the post-execution summary.
📈 Metrics (since 2025-06-30T14:18:56Z):
📊 CPU User:
100.0 ┤
87.5 ┤ ╭─╮╭───────────╮
75.0 ┤ ╭╯ ╰╯ │
62.5 ┤ ╭╯ ╰╮
50.0 ┤ │ │
37.5 ┤ │ ╰╮
25.0 ┤ ╭╯ │
12.5 ┤ ╭─────────╮╭─────╯ ╰╮
0.0 ┼────────────────────╯ ╰╯ ╰
CPU User (Percent)
Stats: min:0.0 avg:29.0 max:93.4 Percent
📊 Memory Used:
100.0 ┤
87.5 ┤
75.0 ┤
62.5 ┤
50.0 ┤
37.5 ┤
25.0 ┤ ╭────────╮
12.5 ┤ ╭──╮ ╭──────╯ ╰───╮
0.0 ┼────────────────────────────╯ ╰──────╯ ╰
Memory Used (Percent)
Stats: min:0.5 avg:7.4 max:20.9 Percent
Example full output:
📈 Metrics (since 2025-06-30T14:18:56Z):
📊 CPU User:
100.0 ┤
87.5 ┤ ╭─╮╭───────────╮
75.0 ┤ ╭╯ ╰╯ │
62.5 ┤ ╭╯ ╰╮
50.0 ┤ │ │
37.5 ┤ │ ╰╮
25.0 ┤ ╭╯ │
12.5 ┤ ╭─────────╮╭─────╯ ╰╮
0.0 ┼────────────────────╯ ╰╯ ╰
CPU User (Percent)
Stats: min:0.0 avg:29.0 max:93.4 Percent
📊 CPU System:
100.0 ┤
87.5 ┤
75.0 ┤
62.5 ┤
50.0 ┤
37.5 ┤
25.0 ┤ ╭──╮
12.5 ┤ ╭╯ ╰──────────────╮
0.0 ┼────────────────────────────────────╯ ╰───
CPU System (Percent)
Stats: min:0.2 avg:5.0 max:33.7 Percent
📊 Memory Used:
100.0 ┤
87.5 ┤
75.0 ┤
62.5 ┤
50.0 ┤
37.5 ┤
25.0 ┤ ╭────────╮
12.5 ┤ ╭──╮ ╭──────╯ ╰───╮
0.0 ┼────────────────────────────╯ ╰──────╯ ╰
Memory Used (Percent)
Stats: min:0.5 avg:7.4 max:20.9 Percent
📊 Disk Used:
100.0 ┤
87.5 ┤
75.0 ┤ ╭──────────────────────────────────────────────
62.5 ┤ ╭──╯
50.0 ┤ ╭─────╯
37.5 ┼───╯
25.0 ┤
12.5 ┤
0.0 ┤
Disk Used (Percent)
Stats: min:35.6 avg:68.7 max:75.8 Percent
📊 Disk Inodes Used:
481238 ┤ ╭───────────────────────────────────────────────
450852 ┤ ╭╯
420466 ┤ │
390080 ┤ ╭╯
359694 ┤ │
329307 ┤ ╭╯
298921 ┤ ╭╯
268535 ┤ ╭───╯
238149 ┼───╯
Disk Inodes Used (Inodes)
Stats: min:238149.0 avg:440393.1 max:481238.0 Inodes
📊 Disk IO Time:
10000 ┤ ╭─╮
8750 ┤ ╭╮ ╭╯ ╰╮
7500 ┤ ││ ╭╯ │
6251 ┤ ││ │ │
5001 ┤ ╭╯╰╮ ╭╮ ╭╯ │
3751 ┤ │ │ ││ │ ╰╮
2502 ┤ │ │╭╯╰─╯ │
1252 ┤ ╭╯ ╰╯ ╰╮ ╭──╮
2 ┼─╯ ╰────────────────╯ ╰────────────────────
Disk IO Time (ms)
Stats: min:1.0 avg:1581.3 max:10000.0 ms
📊 Disk Reads:
1472 ┤ ╭╮
1288 ┤ ││
1104 ┤ ││
920 ┤ ╭╯│
736 ┤ │ ╰╮
552 ┤ │ │
368 ┤ │ │ ╭─╮
184 ┤ ╭╯ ╰╮ ╭╯ ╰─╮
0 ┼─╯ ╰───────╯ ╰───────────────────────────────────────
Disk Reads (Ops/s)
Stats: min:0.0 avg:81.8 max:1519.0 Ops/s
📊 Disk Writes:
18816 ┤ ╭─╮
16465 ┤ ╭──╯ ╰╮
14113 ┤ ╭╯ ╰╮
11762 ┤ ╭╮ ╭╯ │
9411 ┤ ││ │ │
7059 ┤ ╭╯╰╮╭╯ ╰╮
4708 ┤ │ ││ │
2356 ┤ ╭╯ ╰╯ │ ╭───╮
5 ┼─╯ ╰────────────────╯ ╰────────────────────
Disk Writes (Ops/s)
Stats: min:4.0 avg:3373.4 max:19192.0 Ops/s
📊 Network Received:
934237025 ┤ ╭╮
817458485 ┤ ││ ╭─╮
700679945 ┤ ││ ╭╯ │
583901406 ┤╭╯│ │ │
467122866 ┤│ ╰╮ │ │
350344327 ┤│ │ ╭╯ ╰╮
233565787 ┼╯ │ │ │
116787247 ┤ │ │ │
8708 ┤ ╰──╯ ╰───────────────────────────────────────────────
Network Received (Bytes)
Stats: min:8707.0 avg:91377905.1 max:950344235.0 Bytes
📊 Network Sent:
1866827 ┼╮
1634232 ┤│
1401638 ┤╰╮
1169043 ┤ │
936449 ┤ ╰╮
703854 ┤ │ ╭──╮
471259 ┤ ╰╮ ╭╯ │
238665 ┤ │ │ ╰╮ ╭╮
6070 ┤ ╰──╯ ╰────────────────────────╯╰─────────────────────
Network Sent (Bytes)
Stats: min:6068.0 avg:159559.6 max:1866827.0 Bytes
Available on RunsOn Linux and Windows runners.
Configures sccache so that you can cache the compilation of C/C++ code, Rust, as well as NVIDIA's CUDA.
The only parameter it can take for now is s3, which will auto-configure the S3 cache backend for sccache, using the RunsOn S3 cache bucket that comes for free (with crazy speed and unlimited storage) with your RunsOn installation.
Example:
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache
env:
CARGO_INCREMENTAL: "0"
steps:
- uses: step-security/runs-on-action@v2
with:
sccache: s3
- uses: step-security/sccache-action@v0
- run: # your slow rust compilationFor Rust, disable incremental compilation as shown above. Run this action before the installer or any command that starts the sccache server: a running server keeps its startup configuration. This action exports settings; the separate installer supplies the executable.
Possible values:
s3- Use RunsOn S3 cache bucket for sccache backend- Empty string - Disable sccache configuration (default)
What this does under the hood is the equivalent of:
echo "SCCACHE_GHA_ENABLED=false" >> $GITHUB_ENV
echo "SCCACHE_BUCKET=${{ env.RUNS_ON_S3_BUCKET_CACHE}}" >> $GITHUB_ENV
echo "SCCACHE_REGION=${{ env.RUNS_ON_AWS_REGION}}" >> $GITHUB_ENV
echo "SCCACHE_S3_KEY_PREFIX=cache/sccache/${{ github.repository_id }}/linux-x64/v1" >> $GITHUB_ENV
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENVThe action scopes compiler cache objects per repository and per runner platform:
cache/sccache/<repository id>/<runner os>-<runner arch>/v1
The repository id is used rather than the owner/name slug so that renaming or transferring a repository does not invalidate its cache; the action falls back to the slug (as two key components, <owner>/<name>) when GITHUB_REPOSITORY_ID is not exposed. The trailing v1 is a layout version, so a future change to the key layout can be rolled out without reusing existing objects.
This is operational isolation, not a security boundary: repositories sharing a RunsOn stack still share the bucket and the runner IAM role. It provides per-repository cache ownership, growth and cost attribution, targeted invalidation, and freedom to change one repository's cache layout without touching the others.
Previously every repository on a stack shared the flat cache/sccache prefix. Moving to the scoped layout starts one cold cache per repository and platform. Objects written under the old prefix are left to the stack's cache lifecycle rule, which expires everything under cache/ after S3CacheExpirationInDays (10 by default).
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:
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=20gb
steps:
- uses: actions/checkout@v7
- uses: step-security/runs-on-action@v2
with:
sticky_cache: |
go
nodeEach 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.
with:
sticky_cache: |
go
node
buildkit
custom,path=vendor/custom-cache,path=~/.cache/my-toolOn RunsOn runners, the action fails if the sticky disk contract is absent or
the disk does not become ready before sticky_wait_timeout. If the runner
reports that the requested disk is unavailable, the action warns and skips all
sticky cache operations so the job can continue cold. On any other runner (for
example a workflow falling back to GitHub-hosted runners), the action skips all
operations and exits successfully, so the same workflow keeps working without
sticky caches. 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/cache, 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 |
Current workflow repository history (local Git proxy serving the workflow SHA from the sticky disk) |
git-full |
Full Git repository mirrors for workflows that need arbitrary refs or repositories | |
gradle |
~/.gradle/caches, ~/.gradle/wrapper |
|
maven |
~/.m2/repository |
|
playwright |
~/.cache/ms-playwright |
|
tool-cache |
$RUNNER_TOOL_CACHE (toolchains installed by setup-* actions) |
|
custom |
One or more paths supplied with path= |
The tool-cache mode persists toolchains installed through GitHub's tool
cache. Run this action before actions such as actions/setup-go,
actions/setup-node, or actions/setup-python:
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=tools-ubuntu24:20gb
steps:
- uses: actions/checkout@v7
- uses: step-security/runs-on-action@v2
with:
sticky_cache: tool-cache
- uses: actions/setup-go@v7
with:
go-version: '1.25.1'This mode mounts an empty or restored sticky directory directly over the
runner-provided RUNNER_TOOL_CACHE path, and persists only the toolchains
installed after the mount. It does not copy toolchains from the runner image
into the sticky cache, so the image's preinstalled toolchains are hidden for
the rest of the job: setup-* actions download any version they need, the
GOROOT_* variables (and, on Linux, the default go linked into /usr/bin)
point to missing directories, and github/codeql-action downloads its CodeQL
bundle. Use it for jobs that install a large toolchain the image does not ship.
It supports Linux and Windows and does not cache package dependencies or build outputs. Use a sticky-disk name tied to the runner image, as restored binaries may not be compatible with another operating system image.
The buildkit mode prepares the state volume used by step-security'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:
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=docker:20gb
steps:
- uses: actions/checkout@v7
- id: runs-on
uses: step-security/runs-on-action@v2
with:
sticky_cache: buildkit
- uses: step-security/setup-buildx-action@v4
with:
name: ${{ steps.runs-on.outputs.buildkit-builder }}
version: v0.34.1
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.31.1
cleanup: false
- uses: step-security/docker-build-push-action@v7
with:
builder: ${{ steps.runs-on.outputs.buildkit-builder }}
context: .
load: trueSticky 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, even without a sticky disk. On
Linux, when the RunsOn ecr-pull-through extra has a Docker Hub prefix
configured, the runner agent writes Buildx's standard
~/.docker/buildx/buildkitd.default.toml before the job if that file does not
already exist. docker/setup-buildx-action discovers that file automatically,
so sticky and regular docker-container builders use the prefixed ECR Docker
Hub cache without an action-specific mirror URL or inline configuration.
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.
The git mode accelerates actions/checkout without changing the checkout step. It starts a local Git proxy whose bare repository lives on the sticky disk, and rewrites https://github.com/ fetch URLs to it (global url.insteadOf). The cache starts with the complete history reachable from GITHUB_SHA, so deeper shallow checkouts work without downloading every ref. GitHub still supplies the live ref advertisement. When the workflow repository requests additional branch or tag tips, including with fetch-depth: 0, the proxy adds those objects under its private scoped namespace and serves the checkout locally. Secondary repositories and exact SHAs unavailable from current branch or tag tips go upstream unchanged.
Use git-full when the workflow must discover or checkout arbitrary refs, or when it repeatedly checks out secondary repositories. This mode preserves full mirror behavior: every requested github.com repository and all of its refs are cached. Its cold download and disk usage can be much larger.
Unlike other modes, the git mode must run before actions/checkout:
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/sticky=20gb
steps:
- uses: step-security/runs-on-action@v2
with:
sticky_cache: git
- uses: actions/checkout@v7To 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.
Repeated invocations share the same job-level cost-reporting claim, so the second invocation does not calculate or print the job cost again.
Notes and limitations:
- Mirror syncs authenticate with the
tokeninput (default:${{ github.token }}). In both Git modes, checking out other private repositories requires passing a PAT with access to them, since the rewritten URLs no longer match the credentials configured byactions/checkout. git pushis 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 positive Go duration (default15m, matching the runner agent's attachment window)
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.
