Uh oh!
There was an error while loading. Please reload this page.
fix(ci): schedule Test Core on a scripts-only diff (#9829) - #10014
Conversation
Six cross-package test-input declarations are rooted at `scripts/`, and the `--union-into` step that pulls their packages back into the affected set is a step INSIDE the `test` job — a job the `core` paths-filter never let start for a `scripts/`-only diff, because `core` names no path under `scripts/`. Adds a fourth filter output, `scripts: ['scripts/**']`, and ORs it into the `test` job's condition only. Build Core, Dogfood and Temporal Conformance stay filtered exactly as they are today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — ACCEPT. Ruling 3 caught a wrong premise in my own dispatch, and the cost is real. Arming.Verified at ⭐ Ruling 3 — my premise was wrong, and you checked instead of inheriting itI told you to verify picomatch against the installed version, citing #9880's measurement under 4.0.5. You found the version that actually matters is a different one:
The filter is evaluated inside the action, not by the repo's node_modules — so both #9880 and my dispatch were reading the wrong instrument. And rather than redo everything under 2.3.1 and declare victory, you ran every table under both and had the census throw on disagreement:
Including ⭐ H2 — one hole was five
And the ten outside this card's scope went to #10015, not into this diff. Ruling 4 said name it and price it, do not add it — and Also: the card said three scripts-rooted declarations; there are six. The premise grew rather than decayed. ⭐ |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9829
What changed
Three edits to
.github/workflows/ci.yml, nothing else in the tree:scripts: ${{ steps.changes.outputs.scripts || 'true' }};scripts: ['scripts/**'];testjob's condition, which now ORs the two:!cancelled() && (needs.filter.outputs.core != 'false' || needs.filter.outputs.scripts != 'false').Build Core,Dogfood Regression Gate,Dogfood Verify CLIandTemporal Conformancekeep thecondition they have today. This is option 2 of the card's own list, as adjudicated on the card.
H1 — the before/after pair (same input, two filters, opposite results)
Evaluated with the matcher
dorny/paths-filter@v4uses, against the parsedfilters:block of eachversion of the file, then substituted into the
testjob's ownif::The change is monotone — the full
(core, scripts)truth table over the three values GitHub candeliver (
'true','false', and the empty string a dead filter job leaves behind):Exactly one input class flips, and it flips towards running.
#4928's "skip only when the filterEXPLICITLY said false" posture is preserved on both operands: the job now skips only when both
said false, so a filter-job death still runs everything.
Layer A, the thing the skip was costing, measured directly (
turbo ls --affectedpayload seededwith the zero packages it really returns — see below — and the union step run against it):
Ruling 3 — the picomatch version, and a correction
The card and PR #9880 measured under picomatch 4.0.5, the version in this repo's tree. That is
not the version the action runs:
dorny/paths-filter@v4's committedpackage.jsondeclares"picomatch": "^2.3.1"and itspackage-lock.jsonresolves 2.3.1, which is what ships in theaction's bundled
dist/. A filter validated only against the tree's 4.0.5 is validated against thewrong matcher.
So every measurement in this PR was run twice, under 4.0.5 (
node_modules/.pnpm/picomatch@4.0.5)and under 2.3.1 (
npm pack picomatch@2.3.1, unpacked outside the repo — no dependency was added).The two agree on every row of every table below, including
apps/!(docs)/**, the extglob formthat motivated the warning; the declared-input census throws on any disagreement and did not throw.
#9880's conclusion survives the version correction — but it survived by measurement, not by luck.
H2 — the full skip table, and what it says
One file changed in each top-level entry of the tree (real tracked files, not synthetic paths):
scripts/…scriptsoutput.packages/…examples/…package.jsontsconfig.jsonpnpm-lock.yamlapps/docs/…Build Docs' businesscontent/…content/; see belowdocs/…docs/audits/2026-07-unknown-key-strictness-ledger.md, a declared spec inputskills/…skills/objectstack-formula/**, a declared formula input.github/…(notci.yml).github/workflows/scaffold-e2e.yml, a declared create-objectstack inputturbo.jsonpnpm-workspace.yamlpnpm-lock.yaml, which is incore.objectui-shaConsole Pin Gateowns it.changeset/…lint.yml, which has no paths filter.claude/…,.githooks/…,.vscode/…,docker/…, root*.md,eslint.config.mjs,lychee.toml,osv-scanner.toml,paseo.json,tsup.config.ts,objectstack.code-workspace,.npmrc,.nvmrc,.gitignore,.gitattributes,.lycheeignore,.mcp.jsonlint.yml, unfilteredThe generalisation that makes this durable:
turbo.jsonstates, per task, which paths outside apackage that package's tests read. Instantiating each of those 58
$TURBO_ROOT$/…declarations toa real tracked file and running it through the
corefilter gives the exhaustive answer:So this is a pattern, not one hole: the declaration layer knows about five roots, the scheduling
layer knows about two. This PR closes the
scripts/sixth — the one the card measured — and leavesthe other ten declarations named, priced and untouched (ruling 4). Prices, from the last 100
first-parent commits on
origin/main:core=falsetoday (⇒ newly run Test Core)scripts/**(this PR)content/**.github/workflows/**(other thanci.yml)docs/**skills/**turbo.jsonpnpm-workspace.yamlThe
turbo.jsonhalf — no edit needed, with the measurementPR #9880 flagged five (now six)
$TURBO_ROOT$/scripts/…inputs inturbo.jsonas unreachable. Theyare Layer B, not Layer A, and only Layer A was broken:
CROSS_PACKAGE_TEST_INPUTS+--union-into. Measuredabove: it works, and it now runs, because the job it lives in now starts.
turbo.jsoninputs. Measured live on thistree, turbo 2.10.10:
create-objectstack#testhashes816a14410f216396on a clean tree,14e2a95d6e6747dewith a one-line no-op appended toscripts/sync-template-versions.mjs, and816a14410f216396again after restoring the file — so the restore leg proves the probe, not justthe mutation.
pnpm check:cross-package-test-inputsis green: "12 package(s) read outside themselves, alldeclared, and turbo.json hashes every declared glob."
Both layers were correct all along; the paths-filter upstream was the only thing keeping them from
running.
turbo.jsonneeds no change in this PR, and gets none.H4 — the cost, and why the narrower filter does not exist
Cost of this PR: 30 of the last 100 merges to
mainwould newly schedule thetestjob. That isnot free, and it is not the "near-empty test job" the triage note assumed.
turbo ls --affecteddoes return zero packages for a
scripts/-only diff — measured here on commit52db5df65(one file,
scripts/check-role-word.mjs):{"packages":{"count":0,"items":[]}}, control17854cba0f(apackages/-local commit) returns 14, so the probe is live — but the union step thenadds
@objectstack/specfor everyscripts/path, because spec declaresscripts/**.@objectstack/spechas 414 test files; a coldturbo run test --filter=@objectstack/speconthis shared 4-vCPU container had not finished after 7 minutes. On CI that is one shard doing real
work (the other two exit on the empty-package fast path) plus checkout/install on three runners.
The narrower filter — only the
scripts/paths the test graph actually reads — collapses into thesame thing. The six declarations rooted at
scripts/are:The union of those globs is
scripts/**. A filter derived from the declarations is thereforebyte-identical to what this PR ships, and any narrower hand-written list would be a second
recognizer that disagrees with the declaration it exists to serve — the standing failure family
(#9747), and precisely the "a list you must remember to update" shape #7802 recorded. The only route
to a genuinely narrower filter runs through narrowing
@objectstack/spec's ownscripts/**declaration to what
scripts/dist-freshness.test.tsreally stages, which is a change toscripts/check-cross-package-test-inputs.mjs— the file open PR #9826 is rewriting. Follow-upterritory, deliberately not touched here.
Recommendation: ship
scripts/**as written. 30% of PRs paying one package's suite is the priceof the promise #7802 made; the alternative on the record is the merge queue as first signal, which
has already dequeued a PR and taken two unrelated PRs with it as batch collateral.
Ruling 1 — structural proof, both versions parsed
Not an eyeball of the diff. Both files parsed with the
yamlpackage and compared job by job:The only two fields that differ anywhere in the file:
filter.outputsgains thescriptskey, andtest.ifgains the ORed operand. No job renamed, no step added or removed, norun:touched — sono required context detaches.
Verdict semantics (the dispatch's ruling 2)
No gate's judgment changes.
test-gate("Test Core") decides from shard attestations and thefilterjob result, never from thecoreoutput value, so its logic is untouched; ascripts/-only PR simply now publishes three real attestations instead of three filter-skips. Thechange makes strictly more CI run on strictly fewer inputs skipped, and changes nothing about what
any check accepts or rejects for an input it already judged.
Gates
Run on the final commit,
e9ce81d9d2, tree clean:The gate set was re-derived from the actual change set with
node scripts/pm/dispatch-gates.mjs(no paths passed) after the final commit; it named five families, all run above.
No changeset: CI wiring, nothing published.
Generated by Claude Code