Skip to content

fix(release): the Releases backfill skips a version whose publish is in flight in another run - #21605

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21359-backfill-publish-guard
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21359-backfill-publish-guard

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21359

Clause-②: no

What this does

release-integrity's audit step now asks one read-only question before it sets releases-missing=true: is this version's publish job in flight in another run of release.yml? If it is, or if the answer cannot be read, the step leaves releases-missing unset, 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: read joins the release-integrity job's permissions: (that job only). One if/elif branch in the audit step, at the point where it used to set releases-missing=true.
  • scripts/release-pending-publish.mjs: a new read-only in-flight mode, 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 from release.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)

Writer Run Job Window
Publish 17.6.0 to npm (awaiting approval) 36955885276 (push, head dcc5ef4c) 110678824190 job 02:31:09Z to 03:05:45Z; "Create GitHub Releases" ran 03:03:47Z to 03:05:42Z
Release integrity (audit + no-mint backfill) 36958423332 (push, head 4e530568) 110686494582 audit 03:03:50Z to 03:04:15Z; "Backfill GitHub Releases" 03:04:37Z to 03:05:42Z

The 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: publish needs release-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.mjs already exists, with select, npm-state, unconsumed and sweep modes. Its --self-test is already run by lint.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-wired already counts it, and it stays green.
  • The sweep mode already reads runs and jobs and names the publish job by PUBLISH_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.yml answers JSON with state set to in-flight or clear:

  • In flight means another run of this workflow (the calling run is excluded) holds a job named exactly Publish V to npm (awaiting approval) whose status is neither completed nor waiting. waiting is a job GitHub holds at the release environment: it has run no step, and it runs none before a human approves it. queued is 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 "status in_progress". The queued window sits between the approval and the job's first step, and a job that has started cannot be anything but in flight.
  • Which runs are read. Two readings are joined, because the run-list status filter is not trusted alone: collectWaitingRuns records ?status=waiting answering the job token an empty list while a waiting run existed.
    • The first reading is the runs that ?status=in_progress and ?status=queued list. This is where a repair-lane (dispatch) publish is found.
    • The second is the push run at the version commit or at the first-parent commits after it (head_sha probes, as sweep does). This is where the push-lane publish runs: for 17.6.0 it is 36955885276, at dcc5ef4c.
    • Each candidate run is then read directly, and its jobs are read when it is not completed.
  • It fails closed. Each of these answers in-flight with reason unreadable: a non-200, a malformed body, a filter whose total_count exceeds 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 its detail.
  • It is read-only by construction. It runs over makeHttp({ dryRun: true }), whose HTTP layer refuses any non-GET before it is sent.

File surface

The claim's surface is release.yml (the release-integrity job only) plus the reader script. This PR also edits scripts/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 from release.yml against 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:

node scripts/release-verify-npm.mjs --self-test
  x CONTROL: ...and backfills the missing GitHub Releases
  x the audit requests the Releases backfill and names a tree that is at the version commit
x release-verify-npm self-test: 2 of 96 case(s) failed.

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, the publish job, version-pr, stale-prompts and docker are 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).
  • Ablations. Each mutation went through scripts/ablation-replace.mjs, which checks the anchor hit and restores the file. Each restore was proven with blob equal to HEAD and an empty git diff HEAD.
    1. The audit branch was disabled (elif [ "$(jq -r '.state' ...)" != 'clear' ] became elif false). release-verify-npm --self-test went 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 from 14181e3c2fd2 to 837b21b8739f, then was restored to 14181e3c2fd2.
    2. in_progress was added to the settled statuses. release-pending-publish --self-test went 3 red: the 17.6.0 window, the push-run reading and the dispatch reading. Blob 77a2302b1140 became ebb153b8c49c, then was restored.
    3. The push-run reading was deleted (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 to 77a2302b1140.
  • Live read-only smoke against this repository. publishInFlight was called for 17.6.0 with a plain GET-only client. It made 5 GETs: the in_progress and queued filters (both empty), head_sha probes at 617f25f8a4 (none) and dcc5ef4c5a (run 36955885276), and a direct read of that run, which is completed. The answer was clear / none-in-flight. This checks the response shapes the stubs assume against the real endpoints.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at a5beac190 derived 51 commands; all 51 were run, each exited 0. --ran printed: Run reconciliation — 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN.
  • ESLint, as a narrowed run with its proof.
    • eslint --no-inline-config --format json was run over the two changed .mjs files: 2 files, 0 errors, 0 warnings.
    • ESLint's own isPathIgnored / calculateConfigForFile puts both files in the linted population. The YAML file is outside it.
    • Their config carries no parserOptions.project and no projectService. Type-aware linting is off, so this diff cannot move any verdict on an untouched file.
  • Declared to CI. These were not run locally: the type-check lanes (no TypeScript package is touched), ci.yml Test Core's shard steps, and the families dispatch-gates lists as wide-population or workflow-valued.

Changeset

None. skip-changeset holds because nothing here is published. The diff is .github/workflows/ plus two repo-root scripts/, the root package.json is private, and none of the 69 public packages' files[] names scripts/ or .github/. As a positive control, all 69 of them name dist.

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.yml is outside this card's surface, so the comment is left as it is. Carrier: the next PR that edits that step's comment.
  • Not in this PR: the image half of the same race. Run 36958423332 also built the 17.6.0 image (job 110687103770, 03:05:48Z to 03:09:18Z), while the publish run 36955885276 built it too (job 110687097666, 03:05:49Z to 03:10:02Z). A guard on the publish job alone cannot close that, because the publishing run's docker job starts after publish completes. So image-missing is left exactly as it was. This is reported to the dispatching seat with the readings, and not filed here.
  • No ADR governs this. ADR-0125 decides which push publishes and who approves it; it is silent on the Releases backfill, and this changes neither.
  • The guard sits in a step whose text batteries 12 and 13 execute. A future change to its branch shape reddens those batteries, not only the script's own battery.

Generated by Claude Code

claude added 2 commits October 3, 2026 15:20
…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 3, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 16:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 16:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 37442d4 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21359-backfill-publish-guard branch October 3, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants