ci: cache npm downloads in the validation workflow - #296
Open
AmaadMartin wants to merge 1 commit into
Open
Conversation
The `Use Node.js` step never requested package-manager caching, and `actions/setup-node` only enables it automatically when the root `package.json` declares `packageManager`/`devEngines.packageManager` -- adk-js declares neither. So the npm cache directory was neither restored nor saved, and every job re-downloaded the whole dependency tree from the registry on each of the three matrix legs. Request `cache: 'npm'`. No `cache-dependency-path` is needed: with none set, the action reads the workspace root and picks up the tracked root `package-lock.json`, which is the only lockfile in the repo.
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
No tracking issue exists for this change.
Related: Feat: Establish Node.js version source of truth (.nvmrc, engines.node, CI pins) #133 (this PR is stacked on it — see "Stacked on Feat: Establish Node.js version source of truth (.nvmrc, engines.node, CI pins) #133" below)
Problem: Every
validationrun re-downloads the entire npm dependency tree from the registry, on all three OSes in the matrix. TheUse Node.jsstep never requests package-manager caching:actions/setup-node@v6only enables caching automatically when the rootpackage.jsondeclarespackageManagerordevEngines.packageManager(see thepackage-manager-cacheinput in the action'saction.yml). This repo declares neither — verified:So the npm cache directory is neither restored nor saved, and all 1167 packages resolved by the root
package-lock.json(lockfileVersion: 3) are pulled from the registry on every job, on every push and every PR synchronize — three times over.Solution: Add one input to the existing step:
Why this and nothing else:
cache-dependency-path. With none set, the action'sfindLockFilereads only the workspace root and takes the first ofpackage-lock.json/npm-shrinkwrap.json/yarn.lock. The rootpackage-lock.jsonis the only lockfile tracked in the repo (git ls-filesreturns exactly one), andtests/**/package-lock.jsonis gitignored, so the key input is unambiguous. Adding the input would only create a way to get the key wrong.actions/cachestep, norestore-keysemulation, noid:/echo step for thecache-hitoutput. The action's own log lines are the evidence; extra steps are review noise."packageManager"field added topackage.jsonto trigger the automatic path — that changes package-manager resolution semantics (corepack) for every contributor in order to fix a CI concern.npm installis unchanged. The cached directory is npm's_cacache(the value ofnpm config get cache, resolved at runtime — which is also why Windows needs no special-casing), notnode_modules, so a plainnpm installbenefits without touching the step.Quoting follows the file's existing convention (
python-version: '3.11'in the same step list).Scope. One file, one line, no deletions.
package-lock.jsonis deliberately untouched: it is the cache key input, so regenerating it would invalidate every entry. Verified byte-identical (sha256 7a6d0827…before and after all local work). The identical uncachedsetup-nodestep in.github/workflows/cross-language-integration.ymlis intentionally left alone to keep this diff to a single reviewable file.Collision check (done before writing any code).
gh pr list --state open --limit 300returned 203 open PRs; I diffed--name-onlyfor every plausibly adjacent one. Seven touch.github/workflows/validation.yaml— #133, #250, #237, #235, #207, #204, #178 — and none of them addscache:; they add unrelated steps (deps:check,ts:check, typecheck-tests), a second job, orfail-fast/timeout-minutes. So the cache fix is uncontested.Stacked on #133. #133 ("Establish Node.js version source of truth") already lands the other half of the original task — pinning Node — and does it more thoroughly than a bare
node-version:would:.nvmrc,engines.nodeacross all four manifests, andnode-version-file: .nvmrcin both workflows. It edits the exactwith:block this change edits, so branching frommainwould have produced a textually conflicting hunk and a competing Node pin. This PR is therefore based onfeat/node-version-source-of-truthrather thanmain, and contributes only thecache: 'npm'line, giving the combined step:I deliberately did not override #133's Node major. Once
.nvmrcexists it is the source of truth, and a workflow-levelnode-version:that disagreed with it would be strictly worse. For the record, checked againstnodejs/Release/schedule.json: Node 22Jodis in maintenance (since 2025-10-21) with EOL 2027-04-30, so the pinned line is currently supported; Node 24Kryptonis the Active LTS (EOL 2028-04-30) and Node 20Ironis already EOL (2026-04-30). Whether to move the pin from 22 to 24 is #133's decision to make, not a cache PR's.Two notes that pre-empt the obvious review questions. (1) Cache trust: npm verifies
_cacacheentries against the integrity hashes recorded inpackage-lock.json, and a cache created by apull_requestrun is scoped to that PR's merge ref, so a PR cannot write a cache that a latermainbuild reads. (2) Key scoping: the key isnode-cache-${RUNNER_OS}-${arch}-npm-${hashFiles(lockfile)}, so each matrix leg gets an independent entry and the Node version is not part of the key — changing the pin does not invalidate the cache. npm has norestore-keysfallback, so any lockfile edit is a hard miss and a fresh full cache; that is inherent to the action and acceptable.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
No unit test accompanies this change, deliberately. The diff adds zero executable source: it is one line of GitHub Actions YAML. The vitest coverage
includelist iscore/src/**,dev/src/**,integrations/src/**and this diff touches none of them, so the new-line-coverage rule has no surface to apply to. A test that parsedvalidation.yamland asserted on its own contents would pin the file to itself and prove nothing; the repo has no precedent for testing CI config. The evidence below stands in for it.validation.yamltriggers onpull_request: branches: [main]. This PR's base isfeat/node-version-source-of-truth, notmain, so the workflow does not fire. The cold/warm cache-hit evidence from CI that would normally justify this change cannot be captured from a stacked PR — I am not going to paste numbers I did not measure. It becomes observable the moment #133 merges and this rebases ontomain; see "How to verify once this is onmain" below.What I did measure, locally and honestly:
1. Static verification of the workflow (offline, no new dependency —
js-yamlis already in the tree).Invariants hold: one job, the same 10 steps in the same order, the same three-OS matrix, the same
NODE_OPTIONSenv, the same triggers.2. The cache mechanism actually does the work — proved by making it fail. Timing alone is weak evidence, so I pinned the causal claim with a falsifiable check. In a scratch copy of the root manifests (the repo's own
node_modules/lockfile untouched), against an isolated cache directory:Then the discriminating test —
npm install --offline, which is allowed to touch no registry:added 1081 packages in 14s, exit 0.npm ERR! code ENOTCACHED … cache mode is 'only-if-cached' but no cached response is available.The check flips from pass to fail when and only when the cache is removed, so the cache — not incidental warm-up — is what supplies the packages. Caveats stated plainly: this was run with
--ignore-scripts, and the absolute seconds depend on the local registry and machine, so treat the ratio and the offline/ENOTCACHEDresult as the finding, not the numbers. The populated_cacachemeasured ~97 MB uncompressed; GitHub stores it compressed and creates one entry perRUNNER_OS+arch, so roughly 0.3 GB per distinct lockfile hash against the 10 GB repository budget, with no single entry near 1 GB.3. Local validation of the exact commit pushed (required because CI cannot run on a stacked base). All on Node v22.22.2, the major pinned by the
.nvmrcthis stacks on:npm ciadded 1089 packages, noEBADENGINEagainst #133'sengines.node >=22.0.0; lockfile byte-identical afternpm run buildnpm run lintnpm run format:checkAll matched files use Prettier code style!npx secretlint .github/workflows/validation.yamlThe repo test suite was not run: this diff changes no source it could exercise, and
npm run test:coverageremains gated by CI on the existing thresholds once this reachesmain.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
How to verify once this is on
main(the acceptance criterion, deferred only because of the stacked base):validationrun once. In each of the three OS jobs, expand Use Node.js and confirm the linenpm cache is not found; in the job's post steps, confirm the cache save and note itsnode-cache-<OS>-<arch>-npm-<hash>key and size.package-lock.json, re-run the same PR (Actions → "Re-run all jobs"). Each OS job's Use Node.js step should logCache restored from key: node-cache-<OS>-<arch>-npm-<hash>with the hash from step 1. A cache created on a PR merge ref is only restorable by re-runs of that same PR, so do not try to confirm this from a different branch and do not read a fresh PR's first run as "no hit".npm config get cache(a%LocalAppData%-style path, not~/.npm). Windows caches hold very many small files and restore is the slowest of the three platforms — if the warm Windows job is not faster end-to-end, that should be reported as measured rather than assumed to be a win.To reproduce the local benchmark in §2, copy the root
package.json/package-lock.jsonand the three workspacepackage.jsonfiles into a scratch directory and runnpm install --cache <dir> --ignore-scriptstwice, deleting<dir>before the first run only.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.