feat(check-overlay-deps): gate unresolvable overlay imports - #12
Conversation
…-aliased specifiers
…ble value imports
…ey anchor in the overlay repo
|
Pushed Credit where due . this came out of reviewing #14, where Matt does it correctly. My own final review flagged the same thing independently, so it was on the list twice. The inputs are workflow-authored and therefore trusted today, so this is not a live vulnerability. It is worth not shipping the pattern at all from a public action that private repos consume: a future caller passing a value derived from anything untrusted would inject shell. Verified behaviourally identical rather than assumed. Running the new step body with the env vars the action would set: the broken b4m-bob fixture still exits 1 with exactly two One small thing left unfixed deliberately, so it does not get lost: a bad |
Adds a
check-overlay-depscomposite action. It fails when a premium overlay imports a package as a value that resolves neither from the overlay nor from the host root . the defect that took out every open PR's preview deploy on 2026-08-03.What broke, and why nothing caught it
b4m-bob#92addedimport { Link } from '@tanstack/react-router'andimport { useQueryClient } from '@tanstack/react-query'tosrc/spa/BobReport.tsx. Neither package is declared in b4m-bob'spackage.json; both live in the host'sapps/client. Once the overlay hydrates topackages/premium/bob, neither resolves from that directory and the Next build dies withModule not found.b4m-bob's CI was green, and
ci / Typecheck & Testis a required check with no bypass actor on that repo. Nobody merged red. The check simply could not see the file: b4m-bob'stsconfig.jsonexcludessrc/spa/**andsrc/api/**, andglue_consumerscovers@bike4mind/scripts, not@bike4mind/client. Staging could not catch it either, becausereconcile.ymlkeys desired state on the app repo's SHA alone, so an overlay-only merge never triggers a composed build.How it decides
For every source file in the overlay, parse with the TypeScript compiler and classify each module specifier as a value import or type-only. A value import must resolve: from the overlay's own manifest, from the host root manifest, or via real
require.resolvefrom the importing file's directory. Two allowlists cover specifiers that legitimately resolve undeclared . bundler-aliased (react,react-dom,next/*, which Next rewrites before resolution) and host tsconfig aliases (@server/*,@client/*).Type-only imports are reported as notes and never fail. This matters more than it sounds:
import { Request, Response } from 'express'appears in the same overlay and carries notypekeyword, but both bindings are used only in type positions, so SWC erases the import before the bundler resolves anything. The action reproduces that elision rule from the AST. Skip it and the gate reportsexpress,aws-lambdaandsstas build-breakers on four of six overlays, and gets switched off within a week.One case that is easy to get wrong and is covered: TypeScript uses the same node kind for a class's
extendsclause and forimplements/ interface-extends, so a naive walker treats a base class as erasable. It is not . the prototype chain needs it at runtime. An undeclared base-class package would otherwise pass silently.Validation
Verified red-then-green against the real incident. At b4m-bob
5f2c467the action reports exactly two errors,@tanstack/react-routeratsrc/spa/BobReport.tsx:10and@tanstack/react-queryat:11. the same file and lines as the Turbopack failure in deployer run 30785499528. At b4m-bob PR 107's head it passes.Run across all six overlay trees, 946 files: zero errors on b4m-libreoncology, b4m-overwatch, b4m-pi, b4m-tavern and b4m-optihashi, notes only. Day-one adoption is clean, so there is no pressure to disable it.
32 unit tests, run by a new job in
scripts-ci.yml.No dependency for consumers
The action resolves
typescriptfrom the host tree viacreateRequire, so it must run afterpnpm installand adds nothing to any consumer's install. Thetypescriptinstall in this repo's CI is test-only.Not in this PR
Wiring the step into
overlay-ci.ymlfollows separately so this can be reviewed as a unit. b4m-optihashi runs a bespokeci.ymland gets the step by hand. A deploy-path backstop lands in bike4mind-deployer.Scope worth naming: this asks whether a module resolves, not whether the code typechecks.
src/api/**andsrc/spa/**remain excluded from all type checking in every overlay, which is tracked separately.