CI/CD operations
GitHub Actions cache keys and safe invalidation
Key a GitHub Actions dependency cache to the environment and the reviewed lockfile, then treat a fallback restore as a performance hint that may contain older packages. Run the package manager's lockfile-based install or verification step before testing. Restrict cache writes to trusted jobs and review cache-mode separately from GITHUB_TOKEN permissions. A cache hit says files were restored; it does not prove dependency correctness or a releasable build.
The failure: a fast restore with the wrong dependency state
Imagine a JavaScript repository whose committed lockfile changes after a dependency review. The old cache contains package downloads for lockfile L1; the new revision contains L2. A broad fallback key can restore the L1 cache into the L2 run. That may save downloads, but it cannot establish that the files now present match L2. If the workflow skips installation merely because some cache was restored, tests may run against an accidental mixture. The first rule is to make the lockfile the authority for dependencies, with the cache serving only as reusable input.
GitHub's cache action searches the primary key first, then partial matches and ordered restore keys within the allowed branch scope. An exact primary-key match is different from a fallback restore. The action's cache-hit output identifies an exact primary-key match; it is not a check that the cached bytes satisfy the current lockfile or that the build output is correct. When the primary key changes with the lockfile, the new run can use an older cache as a starting point only if the install step reconciles it with the reviewed dependency declaration.
Choose a path that represents a disposable download store or other reconstructible dependency data. Avoid treating a whole prepared workspace as a trusted product of a prior run. GitHub documents that cache contents are not signed or verified, and a restored cache can affect files that later execute. The workflow should still check out the intended revision, run its locked install or equivalent validation, and execute its normal tests. A cache speeds a step; it should not become the only evidence that the step's inputs are correct.
Sources: Dependency caching reference — GitHub Docs · actions/cache README — GitHub
Build a key that changes when the inputs change
A useful dependency key separates incompatible environments and changes when the lockfile changes. In the illustrative Node 22 scenario below, the operating system, runtime family, package manager, manual epoch, and hash of package-lock.json each have a purpose. The epoch gives maintainers a deliberate way to abandon an old key family after changing the cache layout or discovering a bad cache. The lockfile hash is the routine invalidation signal. The example assumes one lockfile at the repository root; a monorepo needs a path selected for the job's own dependency graph.
# Under a reviewed dependency-cache step; Node 22 is this example's runtime.
with:
path: ~/.npm
key: ${{ runner.os }}-node22-npm-v2-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node22-npm-v2-The primary key names one assumed environment and one lockfile state. The restore key intentionally omits the lockfile hash, so it can retrieve an older cache from the same OS, runtime, package manager, and epoch. That fallback is acceptable only when the later install step verifies or reconstructs the current state from L2. If a partial restore causes more troubleshooting than it saves, remove restore-keys and accept a cold cache after lockfile changes. A cache design is an optimization choice, not a requirement to restore something on every run.
Do not widen the fallback to a generic prefix such as every npm cache in the repository unless you have evidence it helps. A key should also reflect any relevant runtime or toolchain change the job can make without changing the lockfile. GitHub includes a separate cache version derived from factors such as path and compression, but that compatibility stamp does not replace your own dependency key. Review the effective key and restored source in the run log when a stale dependency appears; guessing from a green cache-hit label is insufficient.
Sources: Dependency caching reference — GitHub Docs · actions/cache README — GitHub
Who can write a cache matters as much as who can read it
A cache is shared state within GitHub's branch and event access rules. A low-trust workflow that can write a cache later consumed by a privileged workflow can turn a speed feature into an injection path. GitHub's current dependency-caching guidance gives low-trust triggers, including pull_request_target when it resolves to the default branch, read-only cache access by default. A trusted push workflow can maintain the reusable default-branch cache. Keep the untrusted job as a reader rather than granting it write merely to avoid a cache-save warning.
The cache-mode key makes the intended boundary explicit. read allows restore but not save; write permits both; write-only permits save without restore; none permits neither. GitHub enforces these modes with scoped cache tokens. They are separate from the GITHUB_TOKEN permissions map that controls repository API calls. Giving a job contents: read does not mean it can save a cache, and withholding GITHUB_TOKEN write does not by itself constrain an explicitly write-capable cache-mode. Review both token surfaces when deciding what an event may do.
An omitted cache-mode receives a trigger-based default. GitHub's September 2026 change made explicit modes available, while its low-trust default continues to restrict writes. The important trap is that setting cache-mode: write or write-only on a low-trust event overrides that protective default and can reintroduce cache poisoning. For a pull_request_target, issue_comment, or workflow_run path, first ask whether a trusted push can create the needed cache and the lower-trust job can restore it. Do not turn on write access simply because a save attempt was denied.
A denied cache operation is not necessarily a failed job. GitHub documents that a skipped restore is treated as a miss and a skipped save continues without failing the run; the action records the outcome in the log. That makes cache policy easy to overlook if the only signal watched is the check verdict. Inspect cache-mode and restore/save logs when measuring hit rate or diagnosing a supposedly stale cache. An explicit read-only mode can make the intended behavior legible even when the default currently reaches the same result.
Sources: Dependency caching reference — GitHub Docs · Workflow syntax for GitHub Actions — GitHub Docs · Control GitHub Actions cache access with cache-mode — GitHub Changelog
Restore visibility and retention are separate decisions
GitHub scopes cache lookup by key, cache version, and branch relationships. A pull request can read caches from its relevant base and default branches under the documented rules, while a cache created for a pull request's merge ref has a narrower scope. That distinction matters when comparing a local branch run with a pull request run: they may not see the same cache entry despite using the same printed key. The path and compression version can also make two entries with matching text keys incompatible.
Caches are also subject to size limits and eviction. A miss after an idle period may simply mean an entry was removed, not that the key expression changed. Do not hard-code a remembered retention period or repository quota into incident guidance; GitHub can change those limits and organizations may have different settings. Read the current cache list, key, branch, and applicable policy when diagnosing a miss. Design the workflow to function correctly on a cold run, because every cache is disposable by nature.
Exclude secrets and credentials from cached paths. GitHub warns that people able to open pull requests can access base-branch cache contents, including fork contributors in relevant scenarios. A cache key with a lockfile hash is not an access control list, and an obscure path is not secrecy. If dependency installation produces files containing tokens, choose a different cache path or remove those files before saving. Keep secret management on its own protected surface and prove that the cached directory contains only data you would allow a cache reader to inspect.
Sources: Dependency caching reference — GitHub Docs · actions/cache README — GitHub
Measure the optimization without mistaking it for release proof
To evaluate a cache change, compare a cold run and an exact-key hit for the same job shape, dependency set, and runner environment. Record the revision, effective key, cache-hit status, lockfile, install outcome, test outcome, and elapsed times for the relevant steps. A fallback restore is its own case because it still requires reconciliation and may download changed packages. If the fallback barely reduces install time, simplifying the key may be the better engineering choice. These are measurements of setup work, not evidence that an application was deployed.
Keep the proof boundary clear. The lockfile and install step determine the intended dependency set; tests determine whether the checked-out revision behaves as expected under that set; a release process separately determines what artifact was built and which revision reached the target. A successful cache restore proves only that files were made available to the job. Even an exact match cannot establish that those files are a verified release artifact, and a green build cannot establish that production now serves it. Follow the normal delivery evidence after the job finishes.
If a cache appears poisoned or stale, rotate the manual key epoch, stop reading the suspect family, and rebuild it from a trusted trigger after checking the dependency declarations. An old cache cannot be edited in place under the same key; GitHub creates a new entry under a new key. Review which branches can still restore the old family and whether a broad restore-keys prefix crosses back into it. The fix is complete only when the next run shows the intended source, install result, and tests, rather than merely a newly green cache indicator.
Sources: Dependency caching reference — GitHub Docs · actions/cache README — GitHub
Sources and verification
- Dependency caching reference — GitHub Docs
GitHub documents exact and fallback key matching, branch scope, low-trust cache writes, cache-mode behavior, unsigned cache contents, and current eviction rules. Verified .
- Workflow syntax for GitHub Actions — GitHub Docs
cache-mode is a workflow or job setting enforced with scoped cache tokens, with read, write, write-only, and none values independent of GITHUB_TOKEN permissions. Verified .
- actions/cache README — GitHub
The official action documents cache key, restore-keys, cache-hit output, branch scope, and read-only save behavior. Verified .
- Control GitHub Actions cache access with cache-mode — GitHub Changelog
GitHub announced explicit cache-mode access controls and warned that write modes on low-trust triggers override secure read-only defaults. Verified .