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:

declarations core=false of those, covered by crosspkg
before #10015 64 19 9
after #10015 64 19 19

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions