Uh oh!
There was an error while loading. Please reload this page.
fix(ci): stop the ci-failure self-test minting a failure annotation on every green lint run - #12018
Merged
Merged
Conversation
…n green runs `scripts/pm/ci-failure.mjs --self-test` printed a line of prose that named a workflow-command token literally, inside backticks, documenting what the tool anchors on. The Actions runner does not know a token is being quoted: it parsed it and minted `annotation_level: failure` on a check run whose conclusion was SUCCESS. Measured on `d63b01436` (check-run 97678882948, `Lint & Repo Gates`), that artifact was the ONLY annotation the run carried, and the step has no `if:` and the workflow no `paths:` filter, so it fired on every pull request. Two facts measured on real runs shape the fix and the gate: * the legacy `##[...]` form is parsed ANYWHERE in a printed line -- the token sat at column 18 and was still consumed, with everything before it dropped and the rest of the line taken as the message; * the `::...::` form is parsed only at LINE START -- two mid-sentence ones print on every PR from `check-prerelease-pin-watch` and mint nothing. The prose is reworded so the printed bytes carry no token; splitting the JS string would have changed nothing, because the runner reads the printed line. `scripts/check-self-test-workflow-commands.mjs` closes the class rather than the instance: it runs the self-tests CI runs and scans their real stdout, so the question "is this token printed?" is answered by printing it rather than by a rule about source. A static prefilter only selects which to run, which keeps the step at ~14s against the ~4min the whole population costs. A sweep of all 128 found exactly one live site; the two inert ones redden the moment they move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
marked this pull request as ready for review
August 25, 2026 05:12
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#11886
What was wrong
scripts/pm/ci-failure.mjs --self-testprinted a line of prose that named a workflow-command token literally, inside backticks, as documentation of what the tool anchors on. The Actions runner does not know a token is being quoted. It parsed it and minted a real annotation on a check run whose conclusion was success:The card's premise held in full, and one part of it got worse on re-measure: the card measured this as 1 of 5 annotations on one PR head. On today's
mainhead (d63b01436, both runs) it is 1 of 1 — the two siblingRelease section coveragewarnings have since been fixed, so the only annotationLint & Repo Gatesnow carries is this artifact. Verified in the workflow rather than inherited:lint.ymltriggers onpull_requestwith nopaths:filter, thelintjob carries noif:, and neither does the step.The two measurements that shaped the fix
The card named one site and one token and said outright that the sweep had not been done. It has been done now, and it decided both halves of the design.
1. The legacy
##[...]form is parsed ANYWHERE in a printed line. The source line beginstail with noand the token sits at column 18; the runner still consumed it, dropped everything before it, and took the remainder of the line as the message — which is why the annotation above starts mid-sentence on a stray backtick. So burying the token in prose or indenting it does not make it safe.2. The
::...::form is parsed only at LINE START.scripts/check-prerelease-pin-watch.mjs --self-testprints two lines carrying::error::and::warning::mid-sentence on every PR, and its check run carries zero annotations.The sweep
Every one of the 128 scripts CI runs that ship a
--self-testwas executed and its real stdout+stderr scanned (~4 min):##[cmd]anywhere in a line — parsedscripts/pm/ci-failure.mjs)::cmd::at line start — parsed::cmd::mid-line — measured inertscripts/check-prerelease-pin-watch.mjs)failureis the only level minted, and the reason is mechanical rather than lucky:erroris the only registered command name that appears in a printed token anywhere in the population.The two inert sites are not touched here. They are one re-indentation away from being real, and the gate reddens at the moment they move — which is the moment they start lying. Flagging them where they sit would be a false positive the next author would be right to delete.
The fix
Because the sweep found one live site, the site itself gets the cheap fix: the prose is reworded so the printed bytes carry no token.⚠️ Note for the next author, and recorded in the code: splitting the JS string would have changed nothing — the runner parses the printed line, not the source. The file's own comments spell the token freely, because comments are never printed.
The class is closed by a gate rather than by a convention:
scripts/check-self-test-workflow-commands.mjsruns the self-tests CI runs and scans their real output, so "is this token printed?" is answered by printing it, never by a rule about source — the failure mode this repo keeps meeting (a matching rule that silently stops matching, with the empty finding set as its fixed point) has no purchase on the verdict. A static prefilter only selects which to run; over-selection costs a subprocess and nothing else. Membership is imported fromcheck-self-test-wired.mjsrather than re-derived — one definition, two gates.It carries no registered-command list on purpose: that set is GitHub's to change and a stale copy would under-match in silence. The detectors match the command shape, which over-matches in the safe direction — measured cost on this tree: zero false positives across the whole population.
Cost: ~14 s (14 of 128 selected), against the ~4 min the whole population costs. The measured reason the prefilter's
::arm is not simply/::[A-Za-z]/: that spelling selectedos-verify-lock.sh— a 65-second self-test — onTime::HiRes::time(), a Perl namespace inside a shell string.The pin is non-vacuous — shown red, in both directions
Three legs, scripted under
trap restore EXIT INT TERM, each mutation confirmed on disk by grepping both the injected and the removed text before any reading was taken. No build leg applies: both files are read directly off disk bynode <path>and by a relative source import, so nothing resolves through adist/.--self-test1 finding(s)FAILEDLeg C is the one that makes A mean something: with the defect still on disk and only the detector broken, the gate goes green — so the red in leg A came from the detector matching, not from a refusal or an unrelated path.
Verification
Union re-run on the final commit
e018b0024, tree clean:Plus the derived families for this diff (
dispatch-gates.mjs --repo objectstack-ai/objectstack, 3 paths, 23 families): step-collectors, required-contexts, workflow-status-functions, shard-attestation, aggregator-roster, ci-filter-parity, entry-guard, parse-guard, nul-bytes, cross-package-test-inputs, pnpm-filter-targets, agent-test-spelling, node-version, pnpm-acquisition, whole-set-label-write, drift-comment — allexit=0.Ratchet family and lint at the same sha, after building the closure (
turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70 successful) because the ratchet refuses to measure an unbuilt tree:Exit codes captured before any pipe throughout. No changeset: this changes
scripts/and one workflow, and publishes nothing —skip-changeset.Generated by Claude Code