fix(release): the Releases backfill skips a version whose publish is in flight in another run - #21605
Merged
objectstack-fleet[bot] merged 2 commits intoOct 3, 2026
Conversation
…blish is in flight The new read-only `in-flight` mode reads the runs of release.yml that the in_progress and queued filters list, plus the push run at the version commit, and answers in-flight while another run holds that version's publish job in a state other than completed or waiting. Any answer it cannot read answers in-flight with reason unreadable. It runs over the dry-run HTTP layer, so a non-GET cannot leave it. A new self-test battery pins it (12 cases). Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…in flight in another run release-integrity's audit now asks `release-pending-publish.mjs in-flight` before it sets releases-missing. npm settles before the publish job reaches its own "Create GitHub Releases" step, so a landing audited in that window used to write the same Releases beside it (17.6.0: run 36958423332 against the publish of run 36955885276). An in-flight publish, or an answer that cannot be read, leaves releases-missing unset with a notice or warning; the step stays green and the image request is untouched. The job gains `actions: read`. release-verify-npm.mjs batteries 12 and 13 run the audit step's own text, so their stubs now answer the Actions read; battery 12 gains seven cases pinning the branch (in flight, unreadable, another version, the control, and no read on the common path). Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 3, 2026
This was referenced Oct 3, 2026
objectstack-fleet
Bot
deleted the
claude/issue-21359-backfill-publish-guard
branch
October 3, 2026 17:07
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 #21359
Clause-②: no
What this does
release-integrity's audit step now asks one read-only question before it setsreleases-missing=true: is this version's publish job in flight in another run ofrelease.yml? If it is, or if the answer cannot be read, the step leavesreleases-missingunset, says why in a notice or a warning, and stays green. So the Releases backfill and its ADR-0087 D4 sibling no longer write beside a running publish. Nothing else in the job changes. The image request is untouched, and the guard never fails the step or refuses anything..github/workflows/release.yml:actions: readjoins therelease-integrityjob'spermissions:(that job only). Oneif/elifbranch in the audit step, at the point where it used to setreleases-missing=true.scripts/release-pending-publish.mjs: a new read-onlyin-flightmode, and a new self-test battery (12 cases). The battery roster floor rises from 20 to 21.scripts/release-verify-npm.mjs: batteries 12 and 13 run the audit step's own text fromrelease.yml, so their stubs now answer the Actions read. Battery 12 gains 7 cases pinning the new branch, and its floor rises from 17 to 24. See "File surface" below.The premise, measured (read-only
GETs on the runs and jobs)Publish 17.6.0 to npm (awaiting approval)36955885276(push, headdcc5ef4c)110678824190Release integrity (audit + no-mint backfill)36958423332(push, head4e530568)110686494582The two writers are in different runs: the publish runs in the run of the push that carried 17.6.0's version commit, and the backfill runs in a later landing's run. A
needs:edge cannot order jobs across runs, so the API read the card proposes is the right shape. Inside one run the order already holds:publishneedsrelease-integrity, and the audit takes the not-on-npm branch there.Two premises from the card and the claim had moved on
main:scripts/release-pending-publish.mjsalready exists, withselect,npm-state,unconsumedandsweepmodes. Its--self-testis already run bylint.yml("Release version-commit selection self-test"). So the guard is a new mode of that script with its own battery in that script's roster, not a new file.check:self-test-wiredalready counts it, and it stays green.sweepmode already reads runs and jobs and names the publish job byPUBLISH_JOB_NAME. The new mode reuses its HTTP layer (makeHttp,expectOk) and its version-commit walk (versionCommitsOf).The guard's rule
in-flight --version V --version-commit SHA --head SHA --workflow release.ymlanswers JSON withstateset toin-flightorclear:Publish V to npm (awaiting approval)whose status is neithercompletednorwaiting.waitingis a job GitHub holds at thereleaseenvironment: it has run no step, and it runs none before a human approves it.queuedis an approved job waiting for a runner, so it counts as in flight. A status the script does not know also counts as in flight. This is a little wider than the card's literal "statusin_progress". Thequeuedwindow sits between the approval and the job's first step, and a job that has started cannot be anything but in flight.collectWaitingRunsrecords?status=waitinganswering the job token an empty list while a waiting run existed.?status=in_progressand?status=queuedlist. This is where a repair-lane (dispatch) publish is found.head_shaprobes, assweepdoes). This is where the push-lane publish runs: for 17.6.0 it is36955885276, atdcc5ef4c.in-flightwith reasonunreadable: a non-200, a malformed body, a filter whosetotal_countexceeds what it listed, a run left unread, a version-commit walk that disagrees with the audit's version commit, or a shallow clone. The audit then backfills nothing, with a::warning::, and the next landing reads again. A push run that cannot be found is not unreadable: the filters still speak for it, and blocking on it would block that version's repair for good. The answer says so in itsdetail.makeHttp({ dryRun: true }), whose HTTP layer refuses any non-GET before it is sent.File surface
The claim's surface is
release.yml(therelease-integrityjob only) plus the reader script. This PR also editsscripts/release-verify-npm.mjs, only inside its self-test harness (battery 12's stub servers and cases, battery 13's Releases API stub, and the battery-12 docblock and success line). Batteries 12 and 13 execute the audit step's text fromrelease.ymlagainst stubs, so any new external read in that step needs a stub answer. Measured with the workflow change committed and the harness not yet updated:The guard failed closed against an API that was not stubbed, which is the intended behaviour, and it reddened two existing pins. The claim's serial read named
scripts/release-*as free of open PRs, and this file is in that set.scripts/release-github-releases.mjs, thepublishjob,version-pr,stale-promptsanddockerare not touched.Tests (at
a5beac190, the final commit)node scripts/release-pending-publish.mjs --self-test:✓ release-pending-publish self-test: 80 cases across 21 batteries pass.(68 cases before this PR, plus the new battery's 12.)node scripts/release-verify-npm.mjs --self-test:OK release-verify-npm self-test: 103 cases pass across 13 batteries(96 before, plus 7).scripts/ablation-replace.mjs, which checks the anchor hit and restores the file. Each restore was proven with blob equal to HEAD and an emptygit diff HEAD.elif [ "$(jq -r '.state' ...)" != 'clear' ]becameelif false).release-verify-npm --self-testwent 3 of 103 red: in flight, it backfilled; the notice was missing; with the API unreadable, it backfilled. The anchor moved from 1 to 0 and the blob from14181e3c2fd2to837b21b8739f, then was restored to14181e3c2fd2.in_progresswas added to the settled statuses.release-pending-publish --self-testwent 3 red: the 17.6.0 window, the push-run reading and the dispatch reading. Blob77a2302b1140becameebb153b8c49c, then was restored.for (const id of pushRuns) ids.add(id);). The same self-test went 2 red: the 17.6.0 window found only through the push run, and the jobs-unreadable case. Restored to77a2302b1140.publishInFlightwas called for 17.6.0 with a plain GET-only client. It made 5 GETs: thein_progressandqueuedfilters (both empty),head_shaprobes at617f25f8a4(none) anddcc5ef4c5a(run36955885276), and a direct read of that run, which iscompleted. The answer wasclear/none-in-flight. This checks the response shapes the stubs assume against the real endpoints.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata5beac190derived 51 commands; all 51 were run, each exited 0.--ranprinted:Run reconciliation — 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN.eslint --no-inline-config --format jsonwas run over the two changed.mjsfiles: 2 files, 0 errors, 0 warnings.isPathIgnored/calculateConfigForFileputs both files in the linted population. The YAML file is outside it.parserOptions.projectand noprojectService. Type-aware linting is off, so this diff cannot move any verdict on an untouched file.ci.ymlTest Core's shard steps, and the familiesdispatch-gateslists as wide-population or workflow-valued.Changeset
None.
skip-changesetholds because nothing here is published. The diff is.github/workflows/plus two repo-rootscripts/, the rootpackage.jsonis private, and none of the 69 public packages'files[]namesscripts/or.github/. As a positive control, all 69 of them namedist.Acceptance notes
lint.yml's comment above "Release version-commit selection self-test" says the script "holds all three decisions". After this PR it holds a fourth, the backfill's guard.lint.ymlis outside this card's surface, so the comment is left as it is. Carrier: the next PR that edits that step's comment.36958423332also built the 17.6.0 image (job110687103770, 03:05:48Z to 03:09:18Z), while the publish run36955885276built it too (job110687097666, 03:05:49Z to 03:10:02Z). A guard on the publish job alone cannot close that, because the publishing run'sdockerjob starts afterpublishcompletes. Soimage-missingis left exactly as it was. This is reported to the dispatching seat with the readings, and not filed here.Generated by Claude Code