Skip to content

Commit 4c8cdb2

Browse files
os-support-aiclaude
authored andcommitted
fix(ci): gate cross-package conformance scans by their real input set (#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.
1 parent 0e79785 commit 4c8cdb2

5 files changed

Lines changed: 701 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,34 @@ jobs:
307307
echo "Affected-set diff base: $SCM_BASE (merge-base of origin/$BASE_REF and HEAD)"
308308
echo "Frozen payload base.sha: $PINNED_BASE_SHA -- $BASE_REF has moved $DRIFT commit(s) since it was frozen, and that drift is exactly what this step used to charge to this PR."
309309
TURBO_SCM_BASE="$SCM_BASE" pnpm exec turbo ls --affected --output=json > "$RUNNER_TEMP/turbo-ls.json"
310+
# `turbo ls --affected` answers "which packages does the dependency
311+
# GRAPH reach from this diff" — and some suites read files the graph
312+
# does not connect them to. spec's api-methods-batch-conformance scan
313+
# walks every `*.object.ts` in the monorepo while spec declares no
314+
# dependency on the packages it judges (nor should it: the scan reads
315+
# source text precisely to avoid inverting the spec -> * direction).
316+
# A platform-objects-only diff therefore left it unrun, and #7769
317+
# landed a violation on `main` that only PRs touching `spec` ever saw.
318+
# Measured on turbo 2.10.7: 51 packages affected by that diff, spec
319+
# not among them.
320+
#
321+
# So packages that declare a cross-package input radius are unioned
322+
# back in when the diff touches it. The declarations, and the static
323+
# detector that refuses to let a new cross-package scan go
324+
# undeclared, live in the script (`pnpm check:cross-package-test-inputs`).
325+
#
326+
# Failure here falls back to the FULL package list, never to the
327+
# affected-only set: same posture as the merge-base fallback below
328+
# (#6195), and the same reason — the full list is a strict superset,
329+
# so doubt costs minutes rather than coverage. This is the FILTER
330+
# CONTRACT's half 1 applied one layer down.
331+
if ! git diff --name-only "$SCM_BASE" HEAD > "$RUNNER_TEMP/changed-files.txt" \
332+
|| ! node scripts/check-cross-package-test-inputs.mjs \
333+
--union-into "$RUNNER_TEMP/turbo-ls.json" \
334+
--changed "$RUNNER_TEMP/changed-files.txt"; then
335+
echo "::warning::Could not union cross-package scans into the affected set; falling back to the full package list for this shard."
336+
pnpm exec turbo ls --output=json > "$RUNNER_TEMP/turbo-ls.json"
337+
fi
310338
else
311339
# Falling back to the FULL package list, never to the frozen
312340
# base.sha. This is not the #4690 silent-skip anti-pattern: that is

‎.github/workflows/lint.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,27 @@ jobs:
537537
- name: Required-context name pin
538538
run: pnpm check:required-contexts
539539

540+
# Cross-package test inputs (#7802). A test that resolves the repo root
541+
# and walks it has an input set far wider than its own package, and BOTH
542+
# of CI's scoping layers judge it by where it LIVES: `turbo ls --affected`
543+
# reaches packages through the dependency GRAPH, and turbo's task cache
544+
# hashes `$TURBO_DEFAULT$`, which is package-local. So spec's
545+
# `*.object.ts` conformance scan went unrun on a platform-objects-only
546+
# diff, AND replayed a cached green in the merge-queue build that runs the
547+
# full package list — a violation reached `main` with every gate green and
548+
# was then charged to the next unrelated PR that happened to touch `spec`.
549+
# Twenty such tests, in eight packages, exist today.
550+
# This gate finds the escaping tests ITSELF, statically — nothing has to
551+
# be registered to be DETECTED — and fails on any package that has one and
552+
# no declared input radius, on a declaration gone stale, on a declared
553+
# glob turbo.json does not hash, and on a radius too narrow to cover the
554+
# paths its own tests name. The point is what happens by default: a new
555+
# cross-package scan nobody remembers to register produces a RED GATE
556+
# naming it and the globs to write, not a silent skip.
557+
# Reads ~2000 test sources; sub-second.
558+
- name: Cross-package test inputs
559+
run: pnpm check:cross-package-test-inputs
560+
540561
# #4248 packaging-hygiene guard. Without a `files` whitelist npm packs the
541562
# whole package directory, and 20 of the 49 publishable packages declared
542563
# none — so consumers installed TypeScript sources, unit tests and build

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"check:workflow-status-functions": "node scripts/check-workflow-status-functions.mjs --self-test && node scripts/check-workflow-status-functions.mjs",
7979
"check:shard-attestation": "node scripts/check-shard-attestation.mjs --self-test && node scripts/check-shard-attestation.mjs",
8080
"check:required-contexts": "node scripts/check-required-contexts.mjs --self-test && node scripts/check-required-contexts.mjs",
81+
"check:cross-package-test-inputs": "node scripts/check-cross-package-test-inputs.mjs --self-test && node scripts/check-cross-package-test-inputs.mjs",
8182
"check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs",
8283
"check:type-check-coverage": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs",
8384
"check:type-check-debt": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs --re-measure",

0 commit comments

Comments
 (0)