Skip to content

ci.yml's crosspkg filter is a second, hand-kept recognizer of CROSS_PACKAGE_TEST_INPUTS — nothing fails when a declaration lands in a root it does not cover #10379

Description

@os-zhuang

Found while implementing #10015 (which makes the ten then-unschedulable declarations schedule). Filed unassigned.

The residual hole

After #9829/#10014 and #10015, ci.yml carries a crosspkg paths-filter whose entries mirror the
top-level roots that CROSS_PACKAGE_TEST_INPUTS declares and core does not already match:

crosspkg:
- 'scripts/**'
- 'content/**'
- 'docs/audits/2026-07-unknown-key-strictness-ledger.md'
- 'skills/objectstack-formula/**'
- '.github/workflows/scaffold-e2e.yml'

pnpm check:cross-package-test-inputs verifies two of the three layers the mechanism has:

  • it finds escaping tests itself, statically, and fails naming any package that has one and no
    declaration;
  • --verify requires turbo.json to carry a matching $TURBO_ROOT$/… input, so the task hash
    moves with the declared path (Layer B).

Nothing verifies the third: that the scheduler can start the job in which Layer A's
--union-into step lives. The five entries above are a second recognizer of the same declarations,
kept in step by hand. Add a declaration in a root none of them covers — tools/, docker/,
paseo.json, a second skills/* bundle, another docs/… file — and the gate stays green, the
turbo hash still moves, and the test still does not run at PR time. That is exactly the #7802
shape, one layer up, and it is the failure #10015 was filed for after #9829 fixed one root.

Measured, on f3996182b

Instantiating every $TURBO_ROOT$ declaration to a real tracked file and running it through
ci.yml's filters with picomatch (the matcher dorny/paths-filter@v4 uses), under both 2.3.1
(bundled by the action) and 4.0.5 (this tree) — the two agree on every row:

declarationscore=falseof those, covered by crosspkg
before #1001564199
after #10015641919

The "after" row is green today, and no gate holds it there.

Two routes, both previously considered

  1. Derive the filter entries from CROSS_PACKAGE_TEST_INPUTS at CI time — option 1 of ci.yml's core paths-filter names no path under scripts/, so a scripts-only PR skips Test Core — and with it the --union-into step three cross-package declarations depend on #9829's
    own list, held there because it collided with fix(ci): reconstruct the paths cross-package tests really read, instead of trusting quoted prose #9826's rewrite of
    scripts/check-cross-package-test-inputs.mjs. fix(ci): reconstruct the paths cross-package tests really read, instead of trusting quoted prose #9826 has since merged, so the collision is
    gone. dorny/paths-filter accepts its filters input as a string, and a YAML flow sequence
    (crosspkg: ["scripts/**", …]) is a single line, so an earlier step could emit it into
    $GITHUB_OUTPUT with no indentation problem. Cost: a new failure mode inside the filter job —
    the one job whose death THE FILTER CONTRACT (filter job 一旦失败,Test Core / Build Core / Dogfood 会全部 skipped 而分支保护判为通过 —— 隐式 success() 今天已第三次咬人 #4928) was written about — and the entries stop
    being readable in the workflow file.

  2. A static parity gate: assert at lint time that every declared glob is covered by core or
    crosspkg. No CI-time coupling, entries stay readable and commented. Cost: it needs a
    coverage decision. Instantiating to tracked files needs a picomatch-compatible matcher for
    core's apps/!(docs)/** extglob, which the gate's deliberately dependency-free
    globToRegExp does not support — i.e. a third recognizer, with its own divergence risk. A
    pure-string rule avoids that: a declared glob is covered iff some list literally contains it, or
    contains <prefix>/** for a directory prefix of its leading literal segments. Checked by hand
    against all 55 unique declared globs on f3996182b, that rule classifies every one correctly —
    red on the exact ten ci.yml's core filter misses ten more declared cross-package test inputs — content/ (7), docs/, skills/, .github/workflows/ #10015 fixes (including .github/workflows/scaffold-e2e.yml, the
    same-root-different-file case a root-level rule would false-green), green on the other 45.

Route 2's pure-string rule looks like the cheaper and safer of the two, but choosing between them
is a mechanism decision, which is why this is a card rather than a rider on #10015's PR.

Refs: #10015 (the four roots), #9829 / #10014 (the scripts/ root), #9826 (the merged rewrite that
unblocked route 1), #7802 (the defect the mechanism exists for), #4928 (THE FILTER CONTRACT).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions