Skip to content

Documentation · Caching

Protecting a sticky disk

Which builds may change a repository's sticky disk, the credentials we refuse to keep, the record of what changed it, resetting it, and protected repositories.

A sticky disk is state that every later build of a repository starts from. This page is about keeping that state trustworthy: which builds may change it, what we refuse to keep, how to see what changed it, how to empty it, and how to keep pull requests away from it altogether.

A sticky disk is state that every later build of the repository starts from, so which builds may change it is decided on our side, not by your workflow file.

  • A successful build of your default branch started by push, schedule, workflow_dispatch, repository_dispatch, delete or page_build, or by registry_package for a package published from the default branch, keeps what it leaves. The next build starts from that. These are the events GitHub itself lets write the default branch's Actions cache. If such a build fails, is cancelled or times out, what it left is thrown away. So is what it left if GitHub has not told us how the build ended within 15 seconds of its runner shutting down; that usually takes a second or two.

  • Every other build of a branch of the repository works on a private copy of the last kept state, which is thrown away when the job ends. Feature branches, pull requests from branches of the repository, pull_request_target from a branch of the repository, issue_comment, workflow_run and merge queues all start warm and change nothing. Discarding the copy never fails the job.

  • A job whose workflow run comes from a fork is not given a sticky disk: a fork's pull request, and pull_request_target for a pull request opened from a fork, which runs your workflow but usually checks out the fork's code. Neither is a job whose workflow run we could not read, because we cannot tell whether it is a fork's, nor a job that runs a tag rather than a branch. GHWARP_STICKY_DISK is empty for all of them.

What a build leaves is kept or discarded according to the build that actually ran, not the job the runner was started for. GitHub hands a runner whichever queued job of your repository matches its label, so the two can differ: a fork's build that GitHub hands a runner prepared for a push to your default branch is judged as the fork's build when it ends, and nothing it wrote is kept. It can still read the copy that runner was given, which is one more reason the disk must hold no credentials, unless the repository is protected (below).

Until a build of your default branch has kept something, every build starts from an empty disk and formats it, so a repository whose first builds are all pull requests stays cold until the default branch has built once.

Before what a build left is kept, the part of the disk it changed is checked for credentials in these formats:

  • a GitHub personal access token (ghp_)

  • a GitHub OAuth token (gho_)

  • a GitHub App token (ghu_ or ghs_)

  • a GitHub refresh token (ghr_)

  • a GitHub fine-grained personal access token (github_pat_)

  • an AWS access key, in a credentials file or an AWS_ACCESS_KEY_ID setting

  • a Slack bot token (xoxb-)

  • a Slack user token (xoxp- or xoxe-)

  • a Slack app-level token (xapp-)

  • a Stripe live secret or restricted key (sk_live_ or rk_live_)

  • an npm access token (npm_)

  • a PyPI API token (pypi-)

  • a crates.io API token (cio), where cargo writes it

If one is found, nothing that build left is kept. The disk stays as the last kept build left it, and the next build starts from there. The job itself is not failed. A build that writes the same credential every time is refused every time, so the disk stops changing until the step that writes it is fixed.

This is a safety net, not a reason to relax the rule above:

  • It reads the disk's bytes, not its files, so it cannot see inside a compressed or encrypted file.

  • It does not look for private keys, including the one in a Google Cloud service-account key file. Dependency caches are full of test keys that are byte for byte the same as real ones, so checking for them would stop almost every disk that caches dependencies from ever being kept.

  • It checks only what the build changed.

When a build's changes are not kept for this reason, the Sticky disks page in your dashboard (under Configuration) says so at the top: which build it was, from which branch and by whom, and which kind of credential. Never the credential itself, which we do not record. If a disk stops picking up what your default branch's builds leave, look there first.

To be told without looking, add A sticky disk refused a build's changes to a notification channel in Settings (email, Slack, Microsoft Teams or a webhook). It names the build and the kind of credential, never the credential, and is sent within the hour, once per repository: not again until one of its builds is kept or you reset the disk.

The Sticky disks page in your dashboard keeps a record of every build that had a repository's disk: the build that actually ran (named by itself, even when GitHub handed it a runner we started for another job), its branch, trigger and the GitHub user who started it, the disk's size, and whether what it left was kept. A build whose changes were not kept says why: it was not a successful build of the default branch, it came from a fork, a credential was found, or the change could not be checked or stored in time. When you reset a disk, the page says when it was emptied, and no build kept before the reset is named as what the disk holds.

If a credential gets onto a disk anyway, or you think the disk holds something it should not, reset it: Settings, Sticky disks, Reset a disk (owners and admins). The repository's next build starts with an empty disk. A build already running finishes normally, and nothing it writes is kept. A reset takes a few minutes to reach the runners, and Settings says when it is done. Afterwards no build can read the old contents. Rotate a credential that was on the disk anyway: a reset stops builds reading it, and does not undo a copy anything made first.

Every build of a repository starts from what its default branch last kept, as described above. That is how pull requests stay warm, and it is what GitHub's own cache does. For a repository whose default-branch jobs handle secrets that GitHub restricts to a protected environment, even reading that state may be too much, so you can protect it. An owner or an admin sets it per repository in your organisation's Settings, under Protected sticky disks, and it is off by default. A change applies to jobs that start after it, usually within a couple of minutes.

In a protected repository:

  • Only a successful build of the default branch started by push, schedule, workflow_dispatch, repository_dispatch, delete or page_build, or by registry_package for a package published from the default branch, gets the repository's disk. It reads it and keeps what it leaves, exactly as above.

  • Every other build gets an empty disk at the same path, formatted on first use and thrown away when the job ends: feature branches, pull requests from branches of the repository, pull_request_target, issue_comment, workflow_run and merge queues. GHWARP_STICKY_DISK is still set, so a workflow that expects the mount keeps working, but nothing a default-branch build left is on it and nothing it writes is kept.

  • A fork's build still gets no disk at all, as in any repository.

A build that GitHub hands a runner prepared for a trusted build of the default branch never reads the protected disk: the disk is held back until we know which build the runner was given, and is not opened to any other. That build runs without a sticky disk. Where a runner cannot hold the disk back like that, the trusted build gets an empty disk too: we never trade the protection for a warm start.

What it costs is warmth: in a protected repository, pull requests and branch builds start cold every time. If we cannot confirm a repository's setting -- for example while our control plane is unreachable -- we treat it as protected until we can.