fix(ci): make the cross-repo closer accept Fixes: like the duplicate-fix guard - #10393
Merged
Merged
Conversation
…e-fix guard The repo parses GitHub's closing-keyword grammar in three places and two of them disagreed about the optional colon. `duplicate-fix-guard.yml` accepted it, `cross-repo-issue-closer.yml` did not, so a merged PR written `Fixes: objectstack-ai/objectui#456` took the closer's exit path 1 -- the same quiet green a body with no cross-repo reference at all produces -- and the foreign issue was lost with no notice on the PR and no annotation anywhere. The guard's comment claimed the colon is GitHub syntax; that claim decided the direction of the fix and was verified rather than inherited. It is true, and the reference is in this repository: PR #10241 merged 2026-08-20T15:10:06Z carrying a `not fixed:` sentence bound to issue #10240, and that issue closed as `completed` at 15:10:08Z with its own closing-link summary naming #10241 alone. So the closer was under-matching and is widened; the guard was already right. Also adds the durable half. A shared module is unavailable -- neither workflow checks the repo out -- so `scripts/check-closing-keyword-parity.mjs` extracts all three parsers from the shipped bytes and asserts they agree behaviourally on the keyword set and the separator, while still refusing what each is scoped to refuse, and sweeps every tracked file for an unregistered fourth. The existing closer pin was blind to this: adding `:?` moved real behaviour and all 105 assertions stayed green, because no scenario had ever put a colon in front of a qualified reference. P1 now carries the spelling and M15 restores the defect. Fixes #9755
os-zhuang
marked this pull request as ready for review
August 20, 2026 17:52
os-zhuang
enabled auto-merge
August 20, 2026 17:53
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 #9755
Two of this repo's closing-keyword parsers disagreed about the optional colon.
duplicate-fix-guard.ymlaccepted it;cross-repo-issue-closer.ymldid not. A merged PR writtenFixes: objectstack-ai/objectui#456therefore took the closer's exit path 1 —No cross-repository closing keywords in this PR body.— the same quiet green a body with no cross-repo reference at all produces. The foreign issue is lost with no notice on the PR and no annotation anywhere.Which parser was right — measured, not inherited
The card's real question was which spelling matches GitHub, because if GitHub rejected the colon then the guard was over-matching and the edit pointed the other way. The guard's comment asserted the colon is GitHub syntax, and a comment is not evidence.
It is true, and the reference is in this repository — found by scanning 1000 closed PR bodies for a colon-form keyword, which returned exactly one specimen:
2026-08-20T15:10:06Znot fixed:sentence bound to issue #10240 (verbatim below)2026-08-20T15:10:08Z,state_reason: completedtotal_count: 1Quoted inside a fence deliberately: GitHub's parser does not fire in fenced blocks (measured here in #8476 step 1), so this citation cannot re-perform the close it documents. Do not lift that line into prose.
Two seconds after the merge, through the colon. So GitHub's parser binds it, the closer was under-matching, and the guard was already right. (The same specimen shows the surrounding prose changes nothing — the sentence said the issue was not fixed and it was closed anyway. That has a live consequence and a cost, both filed; see below.)
The change
cross-repo-issue-closer.ymlgains:?, making its keyword-and-separator prefix identical to the guard's. Its reference scope is untouched — qualifiedowner/repo#Nonly, because the bare form is GitHub's own job.One judgement, and why it is still spelled three times
⭐ The durable fix is one shared spelling, and a shared module is genuinely unavailable here: neither workflow checks the repo out. The closer deliberately never does (
pull_request_target, and its header makes the no-checkout posture an invariant), and the duplicate-fix guard has no checkout step at all — its whole job is onegithub-scriptstep.require()of a repo file resolves to nothing in either. Buying the import by giving the closer a checkout would spend its stated security posture for ten characters of regex.So
scripts/check-closing-keyword-parity.mjs(new) makes the judgement one instead of the text. It extracts all three parsers from the shipped bytes and asserts they agree behaviourally — same keyword set, same separator — rather than asserting three regexes are one string, which they legitimately are not. What differs on purpose (reference scope) is asserted too, with the colon present as well as without, so widening the separator cannot quietly widen the scope.The sweep found a third parser
The card named two. A sweep of all 6119 tracked files for the grammar's two signatures found three parsers plus one non-parser:
cross-repo-issue-closer.github/workflows/cross-repo-issue-closer.ymlduplicate-fix-guard.github/workflows/duplicate-fix-guard.ymlh7-partof-closing-keywordclosingKeywordRe()inscripts/pm/check-half-states.mjsscripts/check-cross-repo-closer-outcome.mjsThe third — the predicate behind
partof-closing-keyword-guard.yml— already accepted the colon, so it corroborates the direction 2-of-3 rather than changing the fix. The sweep is now mechanical: an unregistered fourth parser fails the gate, because "fix these two" is exactly the change that would miss one.The existing pin was blind to this
Adding
:?moved real behaviour andcheck-cross-repo-closer-outcome.mjsstayed green at all 105 assertions — no scenario had ever put a colon in front of a qualified reference. So the fixture now carries it:MIXED_BODY's middle target is reached throughCLOSES:, which makes the colon load-bearing for that body's target arity, andM15restores the pre-fix text and drives P1 red.Reverse verification
Reverted the closer to
origin/mainon a committed tree, re-ran, restored, confirmed byte-identical (git statusclean).check-closing-keyword-paritycross-repo-issue-closer does not accept the OPTIONAL-COLON spelling(one per keyword)check-cross-repo-closer-outcomefinds exactly the three qualified foreign targets, got 2Predicted direction was RED on both, and that is what happened.
Gates — all at
08a11b510, the pushed headDerived with
node scripts/pm/dispatch-gates.mjs(no args, from the merge base), which self-discovered the new gate via its lint.yml registration. Verdict lines as each gate printed them:Declared narrowing (one):
check:type-check-debtwas derived — only because this PR editslint.yml— and refuses to run without a fully built package closure, by its own guard against measuring a different world. This diff contains zero TypeScript, so its ledger cannot move; CI runs it with the closure built. Its siblingcheck:type-check-coverage, same script and same self-test, ran green above.Declared lock usage: every gate above is a pure source-text scanner that neither builds nor reads
dist/, so they ran outsideos-verify-lock.sh. Nothing here built anything.Clause ②: not triggered. The path limb is
packages/spec/src/**and this PR touches none of it; no public/authorable surface changes, no accept/reject contract widens — the closer's recognition of an already-valid GitHub spelling narrows nothing and widens no schema.#9465 epic fence: checked, as the triage comment on the card asked. The fence is
.changeset/**, the changeset scripts,cut-rc.yml,release.yml,pr-automation.yml's changeset steps and rootpackage.json. This PR touches none of them — the new gate is registered inlint.ymlasnode scripts/...rather than apnpm check:*alias precisely because that alias would land in rootpackage.json, following the precedent the adjacent step already documents. So the card is neither fenced nor blocked.Changeset
None —
skip-changeset. Judged by publish surface, not habit: the diff is two workflow files and two repo-rootscripts/*.mjsgates. Nopackages/*file is touched, so nothing publishes.Filed, not folded in
partof-closing-keyword-guard.yml's trigger is narrower than its own stated rationale: it fires only on thePart of #Nspelling, so the specimen above closed a real card and the guard was silent by construction.completedand is not fixed. This seat did not touch that state — it needs a human decision about reopening.Generated by Claude Code