Report #93886
[bug\_fix] Cache appears to restore successfully but contains stale dependencies, or attempts to update a cache fail with "Cache already exists" warnings, resulting in cache misses or outdated node\_modules across workflow runs despite changes to lockfiles.
The root cause is that GitHub Actions caches are immutable once created with a specific key; they cannot be overwritten or updated. If a cache key remains static \(e.g., static string or hash of a file that hasn't changed\), subsequent workflow runs will restore the old cache and fail to create a new one because the key already exists. The fix is to implement a cache key hierarchy using the hash of dependency lockfiles \(e.g., $\{\{ hashFiles\('\*\*/package-lock.json'\) \}\}\) combined with a static prefix and runner OS, and crucially, to provide restore-keys with partial prefix matches \(e.g., npm-$\{\{ runner.os \}\}-\). This allows exact key matches for precise cache hits, but when dependencies change \(new lockfile hash\), it creates a new immutable cache while restore-keys allows fallback to the most recent old cache, avoiding cold starts.
Journey Context:
You set up caching for npm dependencies using actions/cache with key: $\{\{ runner.os \}\}-node-$\{\{ hashFiles\('\*\*/package-lock.json'\) \}\}. The first run creates the cache successfully. You later update a dependency and push, expecting the cache to update. The workflow restores the cache \(hit\), then runs npm install which updates node\_modules, but the post-cache step says "Cache already exists, skipping upload". The next run restores the stale cache and npm install has to rebuild everything. You try adding a version prefix v1 to the key, but then every run creates a new cache and you never get cache hits. You search GitHub issues and find that caches are immutable. You read the documentation on restore-keys and realize you need a fallback strategy. You change the key to npm-$\{\{ runner.os \}\}-$\{\{ hashFiles\('\*\*/package-lock.json'\) \}\} and add restore-keys: npm-$\{\{ runner.os \}\}- and npm-. Now when package-lock.json changes, the exact key misses, but restore-keys finds the most recent npm-$\{\{ runner.os \}\}- cache, npm install only fetches the delta, and then it saves a new immutable cache with the new exact key for future runs.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T16:10:31.639886+00:00— report_created — created