Repository navigation
fix(ci): derive a gate's population through composite actions, not just workflows - #19284
Merged
os-elon-musk merged 1 commit intoSep 20, 2026
Merged
Conversation
…st workflows Six gates rooted their population at `.github/workflows` and none read `.github/actions/**`, so a command executed through a composite action was audited by nothing while every one of them printed a scope line that reads as coverage. The positive control is not vacuous: `.github/actions/setup-pnpm/action.yml` already carries six `run:` steps. - `scripts/pm/dispatch-gates.mjs` now follows `uses: ./.github/actions/NAME` out of a workflow and reads that action's `runs:` steps, recursively and cycle-safe, attributing the invocation to the CALLING workflow (which is what CI schedules) with the action file carried beside it as `viaAction`. - The other five gates are judged from their own sources and extended where the population genuinely belongs: the Node pin census, the step-name quoting scan, the shared self-test population two gates consume, and the `bash -e` masking scan all read both roots now. - Four comments in `.github/**` that named the old blind spot as a constraint are corrected, because this diff is what makes them false. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Sep 20, 2026
Closed
os-elon-musk
marked this pull request as ready for review
September 20, 2026 13:41
os-elon-musk
enabled auto-merge
September 20, 2026 13:42
os-elon-musk
deleted the
claude/issue-19229-derive-through-composite-actions
branch
September 20, 2026 14:18
This was referenced Sep 20, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…n the caller (objectstack-ai#19718) Fixes objectstack-ai#19396 Comment-only, in two files. The stated reason for keeping `actions/setup-node` and the closed-card sweep in the CALLING workflow was a census argument, and PR objectstack-ai#19284 made it false. Each bullet now carries a constraint that was measured on this tree instead. ## The premise, re-measured on the base (`a251aaa19`) All four censuses reach composite actions. Their own scope lines, exit code captured before any pipe: ``` check-node-version EXIT=0 OK (43 setup-node step(s) across 38 workflow(s) and 2 composite action(s), all on Node 22). check-self-test-wired EXIT=0 scope: 311 file(s) under scripts/, 243 carrying `--self-test` in code ...; 226 of those are run by 38 workflow(s) and 2 composite action(s); ... check-workflow-step-name-quoting EXIT=0 OK (scanned 38 workflow file(s) + 2 composite action file(s), 729 step name(s) -- 9 already quoted, 720 unquoted-and-safe). check-step-collectors EXIT=0 511 `run:` steps across 38 workflow(s) and 2 composite action(s); 5 step(s) run 2+ independent self-tests, all of them through a collector. ``` `premise_still_valid: true` — zero composite actions would have refuted the card; both counts read 2. Two further readings, so the repair rests on measurement rather than on the card's guess: - **`scripts/check-node-version.mjs` builds one list, `[...workflowFiles, ...actionFiles]`, and its own header retires the constraint by name:** *"With both roots read, the constraint is gone: put the step wherever the composition wants it."* - **The consequence the sweep bullet drew is false too, measured with the gate's own exported reader.** Read-only probe, nothing in the tree touched: the closed-card step's real bytes handed to `collectInvocations` twice, once as a workflow and once as a composite action file. ``` workflow named=true selfTested=true attribution=["half-state-patrol.yml"] composite action named=true selfTested=true attribution=[".github/actions/half-state-patrol/action.yml"] ``` So a moved step stays in the population, under the composite action's own name. ## The constraint that IS measurable, per step **`actions/setup-node` — the action holds no Node pin, and a `setup-node` step in it would need one.** The floor asserted in `Locate the patrol sources` is read from the `.nvmrc` that shipped with the action (`action.yml` lines 202-217, already in the file and still true). `check-node-version.mjs` holds every `setup-node` pin in this repo — workflows and composite actions alike — equal to that same `.nvmrc`. So a `setup-node` step inside the composite would install the very version the floor is then compared against: `have` and `floor` derived from one file, equal by construction, the comparison unable to fail, and no longer a reading about the CALLER's runner at all. That holds for a sibling pinning the action too, since both the pin and the `.nvmrc` travel at the same sha. This is *not* the card's guess ("a sibling repository's Node pin has to be the sibling's own"), and the guess is not measurable today: **no sibling calls this action.** `objectui` carries its own `.github/workflows/half-state-patrol.yml` that runs its own copy of the sweeper, holds no `.github/actions/` directory at all, and names this action nowhere. The only caller on either tree is this repo's own `uses: ./.github/actions/half-state-patrol`. No gate requires the pin to be a literal in the caller: `check-node-version` accepts `node-version-file: .nvmrc` as the ideal form, fails an unresolvable expression, and cannot reach a sibling repo at all. **The closed-card sweep — the repair is a deletion, which is the card's second exit.** A true reason already sat in both files, first in the sentence: it is the one step of the old file that was never repo-agnostic, it WRITES to cards under a ruling this repo's board took, and no sibling has taken it. The false half (the `check-self-test-wired` population argument) is deleted rather than rewritten, so what remains is what still holds. ## Proof that this is comment-only Parsed YAML compared against the base blob with the repo's own `yaml@2.9.0`, plus a control leg that must differ so the comparison is not one that cannot fail: ``` IDENTICAL .github/actions/half-state-patrol/action.yml raw bytes before=23411 after=23573 (delta 162) parsed JSON length before=10752 after=10752 sha-equal=true IDENTICAL .github/workflows/half-state-patrol.yml raw bytes before=30837 after=30419 (delta -418) parsed JSON length before=5245 after=5245 sha-equal=true CONTROL LEG (one real YAML byte changed, in memory): identical=false — must be false ``` And every changed line is a comment line: ``` changed lines: 56 NON-comment changed lines: (count: 0) ``` No step moved; no `run:`, `uses:`, `with:`, `if:` or `paths:` byte changed. **Line budget:** `action.yml` block 19 lines to 21 (+2); workflow `setup-node` comment 5 to 5 (0); workflow sweep comment 9 to 3 (-6). Net **-4** across the two files, against the suggested ceiling of +12 / -14. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths; the script took the change set from the merge base itself), then reconciled with `--ran`. Exit codes captured before any pipe. Readings are quoted in the report comment on objectstack-ai#19396; the union was re-run on the final commit. `.github/**` ships in no package, so this carries `skip-changeset`: all 70 non-private packages declare an explicit `files[]` and not one entry names `.github` or the repo root. ## Acceptance notes - **`.github/actions/setup-pnpm` is NOT a stale sibling sentence** — checked because the edited sentence cited it. It was already repaired (lines 50-58): it records the blind spot as CLOSED and says the separation is kept for no constraint at all. That difference is now stated in `action.yml`, so a reader who follows the pointer does not generalise setup-pnpm's retired reason to this action, which has a live one. - **`objectui` has not adopted this composite action, and its copy of the sweeper diverges from the one the action ships** — 13,194 lines against 36,717 at `objectui@0cf2d6644` and `a251aaa19`. That is the copy drift this action exists to delete, one board along. Out of scope here and reported for the filing seat rather than touched. --- _Generated by [Claude Code](https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #19229
Authored by Claude Code, session
session_017ef78bLdybu3AffehKkhfk.Six gates rooted their population at
.github/workflowsand none read.github/actions/**, so a command executed through a composite action was audited by nothing while every one of them printed a scope line that reads as coverage. The positive control is not vacuous:.github/actions/setup-pnpm/action.ymlcarries sixrun:steps today, and that file's own header named this gap as the reason it deliberately holds nosetup-nodestep.What the repair is
scripts/pm/dispatch-gates.mjsnow followsuses: ./.github/actions/NAMEout of a workflow and reads that action'sruns:steps, so a command executed through a composite action is derived exactly as one executed inline.paths:filter narrows. An action declares noon:block at all, so attributing to it would invent a schedule nobody wrote. The action file rides alongside asviaActionprovenance a reader can open.uses:a sibling; a one-hop follow would re-open the same blind spot one level down.uses: ./…with no action file behind it is a refusal, not a skip. GitHub refuses to start such a job, so a derivation that dropped it quietly would describe a CI this repo does not have../.github/actions/**is followed. A third-party action's steps are not in this tree. A local action outside that root is a MISSING lead and is refused deliberately: the module's declared inherited population has to stay exactly equal to the trees it really opens, and a follow that could open any directory a workflow names could not be declared at all.The repair the card forbids was not taken: the four live-specimen CONTROL assertions on the sweeper family are untouched — no assertion was re-pointed at a different value-bearing family.
The other five gates — each judged from its source, not from the card's grep
scripts/check-node-version.mjsuses: actions/setup-node@steps, and a composite action is a legal place to write one. The cost was already being paid in the tree rather than merely risked:setup-pnpm/action.ymland three workflows carry comments shaping the composition around this gate's blind spot. Both roots are read; both counts are printed.scripts/check-workflow-step-name-quoting.mjs#inside an unquoted- name:plain scalar. A composite action's steps carry- name:scalars parsed by the same YAML, in the same repo, under the same house style of writing issue numbers into step names.setup-pnpm/action.ymlalone carried eight step names this gate could not see.scripts/check-self-test-wired.mjs.github/actions/**is run by CI in the calling job exactly as an inline one is, so rooting the corpus at.github/workflowsalone made a directory boundary into a coverage boundary — and every#4690floor here fires on an EMPTY population, never on one that is complete-minus-one.scripts/check-self-test-workflow-commands.mjscollectPopulationfrom the gate above and adds no walk of its own (its own-source pin forbids one). What it owns is the DECLARATION: it now declares and pins.github/actionsbeside.github/workflows, so the live coupling covers both roots instead of naming half the corpus to the dispatch derivation.scripts/check-step-collectors.mjsjoin('.github', 'workflows')(line 210 on the filing tree), which is why a literal grep found zero — the population was workflows-only all along. It genuinely belongs: the runner writes a composite action'srun:body to a file and executesbash -eon it, so the abort-on-first-failure masking is the same defect in the same shell. The judgement is shared and only the walk to the steps differs —stepGroups()readsjobs.JOB_ID.stepsandruns.stepsalike..github/actions/absent is not a refusal in any of the five: a repo may legitimately hold no composite action. What keeps the second root from going quiet is a LIVE assertion in each gate's own--self-test(and, forcheck-node-version.mjs, which ships no self-test, the firing/dark control pair recorded below).Firing and dark controls
Each extension has both: the hazard placed inside a composite action is flagged, and the SAME tree with the action file removed is green — which is what makes the first a reading about the second root rather than about the fixture.
check-workflow-step-name-quoting— battery 7, five cases: the#hazard insideaction.ymlis flagged and named by its own path; the same tree with no.github/actions/is green AND not a refusal; aREADME.mdbeside an action is not an action.check-self-test-wired— batterythe composite action corpus, six cases: a--self-testrun only inside an action counts as WIRED and the attribution names the action FILE; the same tree without it reports exactly oneself-test-not-runfinding.check-step-collectors— the bare sequence inside a composite action is flagged asruns (composite); the same pair routed through a collector is green; the real root walk finds a NESTEDaction.ymland names its path.check-node-version— no--self-testships, so the pair was driven by hand in a throwaway git tree (recorded here, ⛔ no temporary file left in the repo):dispatch-gates— a fixture caller that invokes no check of its own derives two families only because the action's steps were read; with the reader answeringnullfor that directory, zero families are derived and the absence is NAMED. Plus the live reading: the discovery pass really opens.github/actions/setup-pnpm/action.ymland really reads its sixrun:steps.A script path that reaches its command through a step
env:value is derived by NEITHER spelling — written inline in a workflow, or written in a composite action. The composite follow makes an action's step read exactly like an inline step, including where an inline step is already not derived. That is a different blind spot and it is pinned here so a green follow is not read as coverage of it.This matters for the card's own beneficiary. Measured against PR #19225 at its head
68ca79ec9(read-only; that PR was not touched):The action spells the sweeper as
node "$SWEEPER"withSWEEPER: ${{ steps.sources.outputs.root }}/scripts/pm/check-half-states.mjsin the step'senv:. The follow reaches the steps; the matcher cannot name a script whose path is an unresolvable expression. So the four pinned CONTROL assertions on that family are restored by this change only if the invocation is spelled so a reader can see it — that is #19225's own repair to make, and it is reported rather than taken here.Tests
All readings taken on this branch at
bc1662577.The long battery, before and after.
node scripts/pm/dispatch-gates.mjs --self-test, run detached withtail --pidand the exit code captured by redirect, never through a pipe:Reverse verification — the new cases can fail. The composite follow was neutralised in the PRODUCTION path (an early return inside
followCompositeActions), the mutation proved on disk before the reading was taken, and the file restored fromHEADafterwards:⛔ No temporary file is left in the repo: the ablation ran from a script outside it, carried a shell
trapon EXIT, INT and TERM that restores the file, and restored withgit checkout HEAD -- PATH(never a baregit checkout --, which restores from the index).Derived gate families.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackover the real change set (10 paths, three-dot vs merge basee6a03e649) derived 51 families; all 51 were run with the exit code captured before any pipe, and reconciled back through--ran:Every one exited 0, including all five extended gates and their self-tests,
check:nul-bytes,check:watch-hint-literal,check:declared-population-liveandcheck:pm-dispatch-gates.pnpm lintis CI's repo-wide run, not this PR's.Acceptance notes
.github/actions/setup-pnpm/action.ymland three workflows carried comments naming the old blind spot as a constraint. This diff is what makes them false, so they are corrected in it. ⛔ The separation itself is kept — the pins are already in place and moving them buys nothing — it is simply no longer forced.action.ymlcarries the same now-stale sentence aboutcheck-node-version.mjs. Not touched: that file belongs to an open PR.skip-changeset: measured, not assumed — 70 published packages, zerofiles[]entries namingscripts/or.github/, root packageprivate: true.Generated by Claude Code