node_modules install reuse in SDK Verification
Issue #1336, raised while
implementing #1286. #1286
made the SDK build reusable across the verification graph; the install
was left repeated, one npm ci per job, and #1336 asked whether reusing a
downloaded node_modules tree is worth the integrity surface it adds.
Decision: no. Do not adopt install reuse. The premise the question rested on does not survive measurement.
What the issue assumed, and what the runners actually do
#1336 estimated "roughly 1.6 minutes per job of setup x 9 jobs", and projected a fully green graph at 66.1 billed minutes against the monolith's 39.6.
Measured over these eight SDK Verification runs (workflow 335749058), summing
every Install dependencies and Install JS SDK dependencies step. The run IDs
are pinned so the figures below can be re-derived rather than re-sampled:
| run | created (UTC) | result | billed | saved if installs were free |
|---|---|---|---|---|
| 32659751859 | 2026-08-23T18:58:41 | success | 57 m | 1 m |
| 32659540648 | 2026-08-23T18:54:41 | failure | 56 m | 1 m |
| 32657626299 | 2026-08-23T18:18:35 | failure | 54 m | 1 m |
| 32607176648 | 2026-08-23T00:10:42 | failure | 55 m | 1 m |
| 32606989166 | 2026-08-23T00:06:16 | failure | 51 m | 0 m |
| 32606986743 | 2026-08-23T00:06:13 | failure | 36 m | 2 m |
| 32606422513 | 2026-08-22T23:53:27 | cancelled | 49 m | 1 m |
| 32606420503 | 2026-08-22T23:53:24 | failure | 34 m | 0 m |
Most of these runs are red — at the time of sampling every PR was failing on a lapsed Kepler audit exception (#1407), unrelated to install cost. That shortens some jobs and so lowers the billed totals, but it does not touch the per-install figure, which is what the decision rests on. The two green-run totals are 57 and 51 billed minutes.
Units matter here, because the issue's estimate was per job. Across 96 jobs
there are 96 install steps, but only 88 jobs install at all: verified
runs none, and mcp runs two (a root npm ci and an MCP-directory npm ci).
The two counts coincide; they are not the same measurement.
| install steps observed | 96 (over 96 jobs, 88 of which install) |
| install seconds, total | 734 s |
| per install step | 7.6 s |
| per job that installs | 8.3 s |
| billed minutes, as measured | 392 |
| billed minutes if every install were free | 385 |
| saving | 7 min across 8 runs — 0.88 min per run, 1.8% |
Against #1336's per-job estimate the comparable figure is 8.3 seconds, not 1.6
minutes — off by a factor of about twelve, and no framing of the units gets
within an order of magnitude of the estimate. actions/setup-node's npm cache
already removed this cost; what remains is linking a mostly-cached tree.
Per job, the spread is flat: 7.6–8.6 s everywhere except Verify: MCP at 11.2 s,
which is the job running two installs.
Why 1.8% is an upper bound, not a target
That figure assumes install becomes free. It cannot. A reused tree still has
to be downloaded, verified against its admission contract, and extracted, and the
tree is far larger than dist/. Realistically the change trades ~8 s of install
for a comparable or greater artifact download and digest verification, so the
true saving rounds to zero.
Billing makes it worse. GitHub bills per started minute per job, so shaving about 8 s off a job removes a billed minute only when that job's wall time sits within those 8 s of a minute boundary. In the sample that happened on roughly one job per run — the "saved" column is 0–2 minutes, and it is 0 for two of the eight runs.
What it would have cost
scripts/lib/sdk-build-evidence.mjs sets the bar for reusing an artifact:
lockfile digest, Node/npm version, platform/arch, expiry, and a tree digest that
catches a mutated or partial extraction, all fail-closed with no prefix or
cross-head fallback. A node_modules tree would need the same contract over a
harder subject — it contains platform-specific binaries and the output of
lifecycle scripts, which is why #1286 deliberately left it out of scope.
That is a new, permanently maintained trust boundary on the path every verification job takes, to recover under 2% of billed minutes, with the downside being a silently wrong dependency tree rather than a slow build.
What to do instead
If the graph's cost is worth attacking, the sample says where the time actually
is. Verify: core, types, styles alone bills 15–17 minutes, dominated by one
Vitest invocation — that is
#1335, and it is roughly
an order of magnitude more billed time than every npm ci in the graph combined.
Standing constraint, whatever is decided later
quickstart-budget must never consume a shared node_modules tree. Its
five-minute clock covers npm ci, browser provisioning, the fixture build, and
the first usable map — a new user's clean install. Handing it an already-linked
tree would delete the npm ci leg of the promise it exists to enforce and let
the lane report success without having measured that leg at all.
To be precise about what "clean" means here, since it is easy to overclaim:
setup-node runs with cache: npm before the clock starts
(.github/workflows/sdk-verification.yml), so the npm download cache is warm.
The lane measures a clean install, not a cold cache. That is the enforced
contract, and it is unaffected by this decision either way.
It is excluded today by construction, since nothing is shared. Any future reuse proposal has to exclude it explicitly.
Reopening this
Worth revisiting only if the per-job install cost changes by an order of
magnitude — a much larger dependency tree, the loss of setup-node caching, or a
registry slow enough that npm ci stops being cache-dominated. Re-run the
measurement above against fresh runs before reopening; the argument here is
entirely a consequence
of the ~8 s figure, not of the integrity concern on its own.