Skip to content

finding(ci): 8 workflow steps run a gate BEFORE pnpm install, but only 1 of them has a test pinning its import graph to builtins — the other 7 break at runtime, in CI, with no local signal #6148

Description

@yinlianghui-tw

Found while doing #6092's PR 2 (the entry-guard sweep). Not fixed there — it is a gate-shaped gap, not part of that conversion.

What was measured

Parsing every file in .github/workflows/ and, per job, comparing each step's index against the first pnpm install step in the same job, 8 steps invoke a scripts/ gate before any install:

workflow : jobstepscript
changeset-guard.yml : no-major1check-changeset-no-major.mjs
changeset-presence.yml : changeset-presence2check-changeset-presence.mjs
ci.yml : changeset-check1check-changeset-fixed.mjs
ci.yml : type-check5check-type-check-coverage.mjs
control-bytes.yml : control-bytes2check-control-bytes.mjs
doc-component-types.yml : doc-component-types2check-doc-component-types.mjs
docs-links.yml : docs-links2check-doc-links.mjs
skills-paths.yml : skills-paths2check-skills-paths.mjs

(lint.yml runs check-entry-guard.mjs pre-install too, per #6133; it is pinned by scripts/__tests__/entry-guard-wiring.test.ts, which asserts the ordering but not the import graph.)

Running pre-install is deliberate and good — it is what lets these gates run unfiltered on every PR shape. The property it depends on is that each one's whole static import graph is node builtins plus repo-relative modules, with nothing needing node_modules.

Exactly one of the 8 has a test pinning that property: scripts/__tests__/check-doc-component-types.test.ts's needs no install case. The other 7 hold the property today by accident of what they happen to import.

Why it matters, and how it fails

A violation is invisible everywhere it could be caught cheaply:

  • not a type errortsc -p tsconfig.scripts.json is happy with a package import;
  • not a lint error — the package is a real dependency of the repo;
  • not a local failure — locally node_modules exists, so the script runs fine;
  • not a test failure — nothing asserts it for those 7.

It surfaces only as ERR_MODULE_NOT_FOUND in a CI job, on whichever PR happens to touch the file — and for the four gates above whose whole point is running on shapes that skip installs, that is a gate that stops running rather than a gate that fails loudly.

The single pinned one proved the class is live: #6092's PR 2 changed check-doc-component-types.mjs and that test went red immediately, on a change that was in fact still install-free. The same change to any of the other 7 would have produced nothing.

Sharper than that: the pin has to follow the graph

The existing assertion read the gate's own import lines and required each to start with node:. That is too narrow in one direction (a relative import of a builtins-only local module is fine) and too weak in the other (it cannot see a package pulled in one hop away). #6092's PR 2 replaced it with a walk of the whole static import graph requiring every leaf to be a builtin, and demonstrated the difference: adding import ts from 'typescript' to scripts/invoked-as.mjs — one hop away — reddens the graph walk and would have been invisible to the old form.

Suggested shape

One gate rather than seven copies of a test. Derive the pre-install step list from the workflows themselves (the parse above is about 25 lines), then walk each named script's static import graph and require every non-relative leaf to be a node builtin. Deriving the list rather than hard-coding it is the part that matters: a hard-coded list rots the first time someone moves a step above pnpm install, which is exactly the edit that needs catching.

A runtime alternative exists and was used as the ad-hoc check in #6092's PR 2 — load each script under a resolver hook that throws on any specifier needing node_modules — but it executes module top level, so the static walk is the better gate.

Refs: #6092 (where this was measured) · #6133 (the pre-install wiring that made the class matter).


Generated by Claude Code

Metadata

Metadata

Labels

ci/cddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repofindingpm:dispatched

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions