fix(ci): require ci / prepublish, so a red pre-publish gate blocks the merge - #35
Merged
Conversation
…e merge The shared pipeline grew a `prepublish` job on 2026-08-05 (cosyte/.github#35, 6142ac4; its second layer defaulted on in #36, 90936ea). `ci.yml` calls that workflow at `@main`, so this repo started emitting a `ci / prepublish` context on every pull request with no commit landing here, and ruleset 19907924 did not name it. The job could fail and the merge still landed on `main`, the branch that publishes. A green check that cannot block a merge is documentation. What it gates is this package's own worst shipped defect: an offline manifest lint that refuses a dependency specifier no registry can resolve, plus a probe that packs this tree and installs the tarball into a clean directory. `@cosyte/cli@0.0.1` and `0.0.2` reached the registry carrying `file:vendor/*.tgz` specifiers and are permanently uninstallable. The manifest lint would have refused both. The order is load-bearing and is recorded, because it is easy to get backwards. Requiring a context nothing emits does not fail a pull request; it leaves it pending and unmergeable forever. A census of the eight most recent head shas here (#27 through #34) finds `ci / prepublish` zero times: the newest of them merged 2026-08-04T22:22:24Z and the upstream job postdates it, so it could not have been read off a real check run any earlier. It is read off the check run on this pull request, and only then written to the ruleset. `ci.yml`'s banner now covers the hazard that actually bit. Every previous wording of it was scoped to splitting a step out of `verify` locally; the `uses:` reference is unpinned, so a job added upstream emits a new `ci / <job>` context here with no commit in this repo, and it always arrives unrequired. Not built, and named as a do-not-build with its reason: a gate inside CI that curls this repo's own ruleset. Anonymous GitHub API is 60 requests per hour per IP and hosted runners share IPs, so it would trade a false green for a flaky red on a required context.
`release.yml`'s trap note told the next releaser that the "Version Packages" PR needs four contexts. The ruleset had six before this branch and has seven after it, so the number has been wrong through at least two changes without anyone noticing. It is load-bearing text: it documents the escape from a real trap (a version PR opened by `github-actions[bot]` arrives with zero check runs, and a required context that never arrives is pending rather than failing, unmergeable by anyone including an admin), so a wrong count sends someone to declare the escape finished early. The count is DELETED, not incremented. A number in a workflow comment has nothing to keep it honest, and a required context the text does not know about is exactly what the reader came here to satisfy. The derivation is written in its place. The same note now records that the escape got more expensive on 2026-08-06: with `ci / prepublish` required, the empty commit it prescribes no longer just re-runs static gates, it packs the tree and installs the tarball of the version about to be published, against the live registry.
…s repo is not in Refuter pass 1 on this branch, three INTRODUCED findings, all addressed here. 1. The trap note in `release.yml` says the "Version Packages" PR "arrives with ZERO check runs" and prescribes an empty commit as the escape. That sentence is pre-existing and false for this repo, and the previous commit made it worse by building a new instruction on top of it. The shared release workflow authors that PR with `RELEASE_PR_TOKEN`, a live cosyte org secret this caller supplies via `secrets: inherit`, so the checks do arrive. Measured on version PRs #24, #26 and #31: each carries exactly one commit, authored by `github-actions[bot]`, and all four workflows started on that commit within about ten seconds, at `run_attempt=1`, with no human commit anywhere. The note now states the measured behaviour, keeps the empty commit only as a diagnosed fallback for a genuinely check-less PR, and gives the command that tells the two apart. This matters because the previous wording told a releaser to push an unnecessary commit onto the branch that publishes, and with `ci / prepublish` now required that push runs a real pack-and-install against the live registry, so a blip would red a required context. That is the do-not-build hazard this item names, reached from the other direction. 2. Adding a required context strands every open PR whose head sha already ran. #33 went BLOCKED and #29 is the same shape, both carrying the seven older contexts green and no `ci / prepublish`. The ordering rule protects PRs opened after the write, not head shas that predate it. Disclosed in agent-notes with the remedy, which is one push per branch. 3. "This repo's merge queue" was wrong: there is no merge queue. What lets a red non-required check merge is `required_approving_review_count: 0` on the ruleset's pull_request rule. Corrected, with the correction named. Also records in `ci.yml` that the upstream `expect-unpublished-deps` default is permanently stale here, because this package declares no `@cosyte/fhir`, and that the obvious way to silence it is the manifest edit CLAUDE.md forbids.
…r minors Refuter pass 2: REFUTED, five INTRODUCED findings. Taking the shape recommendation rather than rewriting the same note a third time. THE CUT. `.github/workflows/release.yml` is restored to its state on d01696d. Its version-PR trap note has now been rewritten twice inside this one slice and produced a fresh false claim each time. The second attempt said the false sentence had stood "for months" and had been "asserted as fact without being checked". Both are wrong: it entered on 2026-07-28 in 62fba77, so it stood 9 days, and it was true when written and exercised the next day. Version PR #21 (2026-07-29) carries 7373b7c, bot-authored with zero workflow runs, followed by 23e1fa6 by Noah, "chore: run CI on the version PR", which is that note's own escape command. So it was wrong for about six days, and it was checked by being used. The rewrite deleted the dated instance that proves the fallback works, which was the real cost. That note needs its own item, with its own measurement of when the PAT arrived and which routes still lead back to a check-less PR. Nothing here makes it worse: it is byte-identical to base. What ships is the part that has survived both passes: the census, the ordering, the required-context table, the ci.yml banner, and the ruleset write itself. FOUR MINORS, fixed where the text survives. 1. The stranded set was wrong. Measured per head sha rather than read off mergeStateStatus: #33, #18 and #16 were stranded by this write, each carrying the six older required contexts green. #29 was already unmergeable on a red `ci / verify` on both matrix legs, and #15 predates `no-emdash` and `no-internal-refs` and was already stranded. Neither is this slice's cost. The table now shows the attribution and says to measure it this way. 2. `required_approving_review_count: 0` was given as the reason a red X merges. It is not: a context absent from `required_status_checks` blocks nothing at any approval count, because the ruleset only evaluates contexts it names. The approval count is why no review is needed, a different question. 3. "Declares no `@cosyte/fhir` in any form" was false. It is declared, as a `file:` devDependency on the vendored tarball, which is what lets this repo's FHIR tests run. The operative fact is that `prepublish-check.mjs` does not read devDependencies at all, so the allowance names something that is not a consumer dependency here. 4. The trap's return routes were stated as if exhaustive; they are not, and `persist-credentials` at its default is one more. That text is gone with the cut.
…slice Pass 3: NOT REFUTED, all five pass-2 findings cleared. Two nits it recorded rather than raised, both fixed here, plus the demonstration this subject was missing. THE DEMONSTRATION. Everything in the branch-protection notes is about a check that reports without blocking. While shipping exactly that fix, the first thing a required context actually blocked was this author's own prose: editing the pull request body turned `no-emdash` red, twice, on a required context, over four U+2014 characters typed into the body. The PR went BLOCKED and stayed unmergeable until the body was rewritten. It is worth recording because it landed on the half of the gate nothing local can see. `scripts/check-no-emdash.sh` scans tracked files and was green throughout, in the pre-commit hook and in verify.sh, and the branch diff carried zero U+2014. The text was never in a file. The PR body exists only on GitHub and is reached only by the `edited` trigger, so no local run of anything here could have caught it. The standing note that the PR body lands under none of the three merge methods is still true; this is what that deliberate over-strictness buys, observed rather than argued. THE TWO NITS. The CHANGELOG said two attempts each produced a fresh false claim about the note's history; only the second did, the first produced a false operational premise. And the stranded-PR table said five PRs were open when six were, the sixth being the PR performing the write.
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.
The defect
cosyte/.githubgrew aprepublishjob on 2026-08-05 (#35,6142ac4; its second layer defaulted on in #36,90936ea).ci.ymlhere calls that workflow at@main, so this repo started emitting aci / prepublishcontext on every pull request with no commit landing in this repo, and repository ruleset19907924did not name it.So the job could go red and the merge still landed on
main, the branch that publishes. A green check that cannot block a merge is documentation.What it gates, and why the hole was the expensive kind
prepublishruns an offline manifest lint that refuses a dependency specifier no registry can resolve, and a pack-and-install probe thatnpm packs this tree and installs the tarball into a clean anonymous directory. Both default on upstream; this caller passes neither input.This package is the reason that gate exists.
@cosyte/cli@0.0.1and0.0.2reached the registry carryingfile:vendor/*.tgzspecifiers and are permanently uninstallable (a published version never moves backwards). The manifest lint would have refused both.The census, measured off real check runs
Read off real
pull_requestcheck runs, never off a workflow'sname:field.Across the eight most recent head shas here (
#27through#34),ci / prepublishappears zero times. The newest of them merged2026-08-04T22:22:24Zand the upstream job postdates it, so it could not have been read off a real check run any earlier, and requiring it earlier would have stranded every pull request pending forever rather than fixing anything.The contexts that do arrive here, all
integration_id: 15368except where noted:ci / verify (22, ubuntu-latest)ci / verify (24, ubuntu-latest)ci / actionlintci / prepublishcodeql / analyze (javascript-typescript)no-internal-refsno-emdashCodeQL(app57789)The order is load-bearing: it runs first, then it is added, in that order. The context name is read off the check run on this pull request, and only then written to the ruleset.
The banner now covers the hazard that actually bit
Every previous wording of
ci.yml's banner was scoped to splitting a step out ofverifylocally. Theuses:reference is unpinned, so a job added upstream emits a newci / <job>context here with no commit in this repo, and it always arrives unrequired. That is now written down where someone would trip it.Not built, deliberately
A gate inside CI that
curls this repo's own ruleset would close the observability gap, and it is not built: anonymous GitHub API is 60 requests/hour per IP, and hosted runners share IPs, so it would trade a false green for a flaky red on a required context, which is worse than the hole. Answer that with a measurement before writing it.Scope, and what is not in it
resolvedSelection()/vitest list --filesOnlydoes not reproduce here: this repo has no test-selection gate at all.pnpm audit --prod --audit-level highis red in the umbrella'sverify.sh, on two advisories under the@modelcontextprotocol/sdkoptional dep. Measured identically red on the parentd01696d, so pre-existing, and it is not a step in CI'sverifyjob. A dependency bump is its own slice.src/is untouched.Two refuter passes, and what they cut
Pass 1: REFUTED (1 major, 2 minor INTRODUCED). Pass 2: REFUTED (5 INTRODUCED). Both are addressed, and the main outcome is that one thing was cut out of this PR rather than fixed a third time.
What was cut, and why
Earlier heads of this branch also edited
.github/workflows/release.yml's version-PR trap note. It is now restored byte-identical to base and is not part of this change.That note carries a stale required-context count and a claim that the "Version Packages" PR "arrives with ZERO check runs". Two attempts to correct it inside this slice each produced a fresh false claim about its own history:
RELEASE_PR_TOKEN, a live org secret supplied viasecrets: inherit, so all four workflows start on the bot's own commit within ~10s atrun_attempt=1(measured on Version Packages #24, Version Packages #26, Version Packages #31). Acting on the false version would have told a releaser to push an unnecessary commit onto the branch that publishes, and now thatci / prepublishis required, that push runs a real pack-and-install against the live registry, so a blip would red a required context. That is this item's own do-not-build hazard reached from the other side.62fba77on 2026-07-28, so it stood 9 days, and it was true when written and exercised the next day: version PR Version Packages #21 (2026-07-29) carries7373b7cb(bot, zero workflow runs) followed by23e1fa6cby Noah,chore: run CI on the version PR, which is that note's own escape command. It was wrong for about six days, and it was checked by being used. The rewrite deleted the one dated instance proving the fallback works.Two rewrites, two fresh false claims, in text that is not what this PR is about. It goes to its own item, where the question that actually needs answering (when the PAT arrived, and which routes still lead back to a check-less PR, with
persist-credentialsat its default being one) can be measured properly.What ships is the part that survived both passes of attack: the census, the ordering, the required-context table, the
ci.ymlbanner, and the ruleset write.The four minors, fixed where the text survives
mergeStateStatus: chore(deps): bump pnpm/action-setup from 4 to 5 #33, chore(deps): bump @modelcontextprotocol/sdk from 1.29.0 to 1.30.0 #18, chore(deps): bump actions/checkout from 4 to 7 #16 were stranded by this write (each carrying the six older required contexts green). chore(deps-dev): bump the dev-dependencies group across 1 directory with 18 updates #29 was already unmergeable on a redci / verifyon both matrix legs; chore(deps): bump actions/setup-node from 4 to 7 #15 predatesno-emdash/no-internal-refsand was already stranded. Neither is this slice's cost. The table now shows the attribution and says to measure it that way.required_approving_review_count: 0is not why a red X merges. A context absent fromrequired_status_checksblocks nothing at any approval count, because the ruleset only evaluates contexts it names. The approval count is why no review is needed, which is a different question.@cosyte/fhirin any form" was false. It is declared, as afile:devDependency on the vendored tarball, which is what lets this repo's FHIR tests run. The operative fact is thatprepublish-check.mjsdoes not readdevDependenciesat all.The count is deleted, not incremented
Where a required-context count survives in this diff it carries its derivation command, because the one in
release.ymlwas wrong through a prior change without anyone noticing.