Skip to content

feat(setup-node-with-cache): add lockfile-only-cache-key input - #258

Draft
sh-waqar wants to merge 1 commit into
mainfrom
PLT-CACHE-KEY/narrow-cache-key
Draft

feat(setup-node-with-cache): add lockfile-only-cache-key input#258
sh-waqar wants to merge 1 commit into
mainfrom
PLT-CACHE-KEY/narrow-cache-key

Conversation

@sh-waqar

@sh-waqar sh-waqar commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in lockfile-only-cache-key input to setup-node-with-cache. Defaults to false, so no existing caller changes behaviour.

The dependency cache key currently hashes the lockfile plus every package.json. In a workspace repo that makes the cache far more fragile than it needs to be — editing a description, bumping a package version, or adding a script invalidates the entire cache, even though none of those change what gets installed.

Measured impact

Replaying both key variants over the last 60 commits on main in bob-the-builder (17 workspaces):

Cache key Commits that invalidate the cache
lockfile + all package.json (today) 30 / 59 = 51%
lockfile only 16 / 59 = 27%

Roughly half the misses disappear.

That repo's cache is ~2.8 GB, and a miss is expensive: every job re-downloads it, runs a full yarn install, and then all four jobs compress and upload ~2.9 GB while only one wins the save race — the other three discard ~10 minutes of work each. Measured cost of a miss there is ~10-12 min of extra wall-clock per run.

Why it's safe

The lockfile already pins the fully resolved dependency tree. Yarn and pnpm rewrite it whenever a manifest edit actually changes resolution, so a package.json change that matters is always accompanied by a lockfile change. A manifest change that doesn't touch the lockfile cannot alter what gets installed.

--frozen-lockfile closes the remaining gap: a manifest edit that would need new resolutions fails the build rather than silently installing something different.

I verified this rather than assuming it. All 14 commits in the sample that touched a package.json without touching yarn.lock:

  • 12 were non-dependency edits (descriptions, scripts, metadata)
  • 2 touched dependency fields, and both added packages that were already resolved in the lockfile — one pinned via root resolutions, one already present as a transitive dep. In both cases the lockfile was unchanged precisely because the package was already installed.

Implementation note

hashFiles() can't take a variable glob, so both candidate hashes are computed in a new dep-hash step and the requested one is exported for the cache steps to consume. The two places that echo the key for logging now read the same output, so the logged key always matches the real one (previously they'd have drifted apart under any key change).

Covers both the yarn and pnpm paths.

Testing

  • YAML validated; step ordering checked (dep-hash runs before both cache steps)
  • Default path produces byte-identical keys to today, so existing callers keep their warm caches
  • The 51% → 27% figure is from replaying real commit history, not a projection

Two unrelated observations from the same investigation

Not fixed here — flagging in case they're useful:

  1. enable-yarn-cache appears to be a dead input. It's declared in inputs: but never referenced anywhere in the action body — cache-mode is the only control. Anyone setting enable-yarn-cache: false today is silently ignored.

  2. Two steps show a consistent ~38s gap before their script runs. On bob-the-builder, Debug cache contents and Check if install needed on cache hit each cost ~37-38s across all four jobs. The delay is before the first line executes:

    22:27:31.400  ##[start-action display=Debug cache contents]
    22:28:09.482  ##[group]Run set +e ...      <- first line of the script
    

    The scripts themselves are only echo / ls -1 / find -maxdepth 0, which can't account for it, and sibling steps in the same composite start in milliseconds. I tried to test whether deleting the step recovers the time, but the probe was invalidated by cache branch-scoping. Raising it as an observation rather than a diagnosis — you may recognise the cause immediately.

The dependency cache key hashes the lockfile plus every package.json. In a
workspace repo that makes the cache far more fragile than it needs to be:
editing a description, bumping a package version, or adding a script
invalidates the whole cache even though none of those change what gets
installed.

Measured on Typeform/bob-the-builder (17 workspaces), replaying both key
variants over the last 60 commits on main:

  lockfile + all package.json (today)   30/59 = 51% of commits miss
  lockfile only                         16/59 = 27% of commits miss

That repo's cache is ~2.8GB, and a miss there costs roughly 10 extra
minutes of wall-clock: every job re-downloads the cache, runs a full
install, and then all of them compress and upload ~2.9GB while only one
wins the save race.

Safety: the lockfile already pins the fully resolved tree, and yarn/pnpm
rewrite it whenever a manifest edit actually changes resolution, so a
package.json change that matters always comes with a lockfile change.
--frozen-lockfile turns the remaining case into a build failure rather
than a silent wrong install. All 14 commits in the sample that touched a
package.json without touching yarn.lock were checked by hand: 12 were
non-dependency edits, 2 added packages already resolved in the lockfile.

Defaults to false, so existing callers are unaffected and keep their
current cache keys.

hashFiles() cannot take a variable glob, so both candidate hashes are
computed in a new dep-hash step and the requested one is exported for the
cache steps to consume. The two places that echo the key for logging now
read the same output, so the logged key always matches the real one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant