fix(ci): gate cross-package conformance scans by their real input set (#7802) - #7928
Conversation
…#7802) A test that resolves the repo root and walks it has an input set far wider than the package it lives in, and both of CI's scoping layers judged such a test by its home package: - `turbo ls --affected` (ci.yml `test`, the PR path) reaches packages through the dependency GRAPH. `packages/spec` declares no dependency on `packages/platform-objects` — and should not; the api-methods batch conformance scan reads source text precisely to avoid inverting the spec -> * direction. Measured on turbo 2.10.7: a platform-objects-only diff makes 51 packages affected, `@objectstack/spec` not among them. - turbo's task cache hashes `$TURBO_DEFAULT$`, which is package-local, so `@objectstack/spec#test` hashed identically before and after the change. Even the merge-queue and push builds — which deliberately partition the FULL package list — replayed a cached green. Measured: `>>> FULL TURBO` in 42ms replaying the prior log, while `--force` on the same tree failed the scan. This layer, not the paths-filter, is why #7769 reached `main`. Packages whose tests read outside themselves now declare that radius once, and both layers are driven from it: ci.yml unions a declaring package back into the shard's package set when the diff touches its globs, and turbo.json carries matching `$TURBO_ROOT$` inputs so the cache hash moves with them. The declaration list cannot rot, which is the point: the gate finds escaping tests itself, statically, and fails on a package that has one and no declaration, on a stale declaration, on a glob turbo.json does not hash, and on a radius too narrow to cover the paths its own tests name. A new cross-package scan that nobody registers produces a red gate naming it and the globs to write — not a silent skip. Census: 20 such tests in 8 packages. Radii are kept narrow so the affected-subset optimisation the 3-way shard exists for survives; the literal-coverage check is what makes narrowing safe.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Check Changeset red — expected, and it needs a label rather than a commit.
Resolution is the Explicitly not taking the two routes that would turn this green wrongly:
No other check has reported a failure on Generated by Claude Code |
Fixes #7802
The card's surviving half: an affected-subset CI keyed on the package a cross-package conformance scan lives in cannot gate that scan. (The spec half —
SINGLE_RECORD_WRITE_ONLY['sys_api_key']— shipped in #7818; nothing here touchespackages/specorpackages/platform-objects.)What I measured, and where the PM's model was wrong
The dispatch's hypothesis was that there are two filtering layers and only the first opens up on
merge_group. Measured atorigin/main, that is not the escape:platform-objects-only diffdorny/paths-filter(filterjob)corematchespackages/**, so it wastrueall alongturbo ls --affected(testjob, PR path)@objectstack/specnot among them@objectstack/spec#testhashes$TURBO_DEFAULT$— package-localmainThe merge queue takes the
elsebranch and partitions the full package list, so it did schedule spec's suite. It just replayed a cached green:turbo run test --filter=@objectstack/specafter the violating edit printed>>> FULL TURBOin 42ms, replaying the previous run's log, while--forceon the identical tree failed the scan. A third layer the card and the dispatch both missed.packages/specdeclares no dependency onpackages/platform-objectsand should not — the scan reads source text precisely to avoid inverting thespec -> *direction. So this is structural: no tuning of a dependency-graph affected set can reach it.The fix
Packages whose tests read outside themselves declare that radius once (
scripts/check-cross-package-test-inputs.mjs), and both layers are driven from it:ci.ymlunions a declaring package back into the shard's package set when the diff touches its globs. Failure falls back to the full package list, never the affected-only set — the same posture as the merge-base fallback (ci.yml 的TURBO_SCM_BASE吃同一个冻结 base.sha —— turbo --affected 在 merge ref 上把 main 漂移算成本 PR 改动(方向保守:多跑,不会少跑) #6195), preserving the FILTER CONTRACT's "when in doubt, run everything".turbo.jsoncarries matching$TURBO_ROOT$inputs, so the cache hash moves with the real inputs.Why this is not just another list someone must remember
It is a list, and a list you must remember to update is exactly what produced #7802. So the list does not rely on memory. The gate finds escaping tests itself, statically, and fails on:
turbo.jsondoes not hash,The default for an unregistered cross-package scan is a red gate naming the file and the globs to write — not a silent skip. Demonstrated below.
The last check is what lets radii stay narrow safely, which is how the affected-subset optimisation survives:
packages/**/*.object.tskeeps spec's 5-minute suite off every PR that does not touch an object, where an always-run carve-out would have put it on all of them. It also caught four real gaps in globs I had authored by hand from a careful census (plugin-authreadscore/src/security/api-key.ts;dogfoodpins spec's zod schemas; …).Census: 20 such tests in 8 packages —
spec(8),dogfood(4),lint(3),cli,core,plugin-auth,plugin-security,create-objectstack. Corroborating the card's "at least the third instance":packages/core/src/security/operation-private-keys.pin.test.tscarries a#7706header about its own scan surface. A name-based grep forREPO_ROOTmissescli's (it usesPACKAGES_DIR) — which is why the detector is structural rather than name-based.Positive demonstration (a green CI run would not prove this)
The failure mode is a scan that silently does not run, so I constructed a diff touching only a package the scan judges but does not live in —
sys_user'sapiMethodslosing'bulk'inpackages/platform-objects— and ran it through the exact pipeline:turbo ls --affected→ 50 packages,partition-test-shardsputs spec on no shard (0/3).+ @objectstack/spec (declared glob matched packages/platform-objects/src/identity/sys-user.object.ts); spec lands on shard 1.>>> FULL TURBO, 42ms, cached green — the scan never executes.sys_user: [get, list, update] grants single-record writes but not 'bulk'(1 failed | 379 passed).>>> FULL TURBO. This does not disable caching; it makes the cache key honest.packages/objectql(registered nowhere) fails the gate with@objectstack/objectql has test(s) that read outside the package but declares no input radius, naming the file.The demo commit was removed; the branch contains only the fix.
Gates
check:cross-package-test-inputs(12 self-test cases),check:changeset-gate-self-tests,check:node-version,check:nul-bytes,check:required-contexts,check:shard-attestation,check:workflow-status-functions,check-changeset-no-major— all pass locally, re-run after rebasing onto0e79785.eslint --no-inline-configon the new script: clean.Notes for review
turbo.jsonship no published package. Needs theskip-changesetlabel..github/workflows/ci.yml(+scripts/). I also touchturbo.json,.github/workflows/lint.ymland rootpackage.json.turbo.jsonis unavoidable — Layer B cannot be fixed anywhere else, and it is the layer that actually let fix(platform-objects,plugin-auth): give API-key revoke/restore a working product route #7769 through; the other two register the gate so it runs. No new required context: the gate is a step in the existinglintjob.spec's liveness ledgers (packages/spec/liveness/*.json) cite repo paths as data, and the literal-coverage check reads test sources only. A ledger citing a file outside spec's declared globs would not be covered. The ledger itself is in-package (so editing it does re-run the suite); only a deletion of a cited file elsewhere could slip. Flagged rather than fixed — closing it means either a repo-wide radius for spec or teaching the gate to read ledger data.Generated by Claude Code