fix(release): queue the publish only on the push that carries the version commit, and publish that commit (ADR-0125 D1 amended) - #20625
Conversation
…pproval prompts that are stale Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
…ion commit, and publish that commit The publish job checks out the version commit and its guard asserts it is on main and is the commit that changed the version; a stale prompt approved anyway refuses; a push-lane prompt still waiting for a version already on npm is cancelled by the new stale-prompts job (ADR-0125 D1 as amended). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
…ueues the publish, and the publish builds that commit Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Contract reviewServed-tier: Head resolved from ① Derived judgmentsPublic surface: none. Four files, exactly the claim's file surface. Every accept-set change is in 1. The predicate (
2. The publish job builds the version commit.
3.
4. Hypothesis 3, checked on the runs. 17.4.0's job (run 34308599522) was created 2026-09-09, started (approved) 07:36:51, cancelled 07:39:36 on 2026-09-29, and held 5. ADR-0125 amendment. Status line 6. 7. Self-test shape. Named batteries with per-battery floors (11 pinned), the handshake flag set as the last statement and read at dispatch, copied not imported: AGENTS.md's required shape. RIGHT. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读
改了什么
为什么改
风险与代价(含回滚)
席位意见
你要做的(一个动作)审阅 ADR-0125 的增补后合并本 PR;或者提交 APPROVED 审批,由席位跑落地前检查后入队。 Generated by Claude Code |
Fixes #20613
Clause-②: no (a CI workflow and an ADR; no contract accept or reject behaviour moves)
What this changes
release.yml's publish lane, per triage5886053015's direction, with ADR-0125 D1 amended in the same PR (Tier H: the maintainer merges).release-integritynow names the version commit: the newest first-parent commit at whichpackages/cli/package.json's version differs from its parent's, i.e. the landing that brought main to its version. On a push,publish-pendingis true exactly when that commit is in this push (not an ancestor ofgithub.event.before) and the version is absent from npm. A later landing queues nothing and evicts nothing. The repair dispatch (D4) keeps "absent from npm" alone, since it is one human act and not one per landing.github.sha. It checks outneeds.release-integrity.outputs.version-commitwithfetch-depth: 0. The guard refuses an empty or mismatched ref (an emptyref:would silently fall back togithub.sha), a commit that is not onorigin/mainas fetched after the approval, and a commit whose parent carries the same version. The release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170 tripwire now runs against the version commit. The Turbo cache key and the run summary name it too.stale-promptsjob (actions: write,contents: read) cancels a push-lane run whosePublish VERSION to npm (awaiting approval)job is waiting atreleasefor a version that is already on npm. It never touches a dispatch run, the calling run, or a job that has started. As a second line, the publish guard refuses a stale prompt that gets approved anyway, outsideforce.scripts/release-pending-publish.mjs(new) holds the three decisions (select,npm-state,sweep), with a battery-floored, handshake-guarded--self-testwired inlint.yml.Amendedstatus line, short notes under D1 and D5, and## Amendment (2026-09-29, #20613)at the end, written in the ADR's own amendment style. It corrects the record and does not reverse it: merging the Version Packages PR is still the decision, and approvingreleaseis still the authorisation.The dispatch's hypotheses, measured
Predicate. Confirmed: it was inline bash reading
github.sha, now replaced as described above. Edge cases are pinned by the self-test and replayed on the real history:7a1faf1a..3a89d459) selects8c87d26aand is pending. The five pushes after it are not pending.beforeall zeros,beforemissing from the clone, orbeforenot an ancestor (a force-push): not pending, with a notice or warning.Every
github.sharead inpublish, with the sha each one now takes:release-spec-changes.sh,release-verify-npm.mjs: they read the workspace, which is now the version commit;release-github-releases.mjsreadsGITHUB_SHA: left unchanged, see Acceptance notes.Concurrency. Measured from the Actions API, and the mechanics differ from the card's wording. The job that takes
release-publish-REFthen waits at the environment while holding the group. Each later publish job pends behind it and evicts the pending job before it, in the same second it is created. On 17.5.0 that happened at 06:48:38, 06:52:42/43, 06:58:40/41, 07:12:44/45 and 07:21:13. So a stale prompt does more than look like a real one: it hides the real one. The 17.4.0 prompt held the group while all of 17.5.0's publish jobs pended behind it.cancel-in-progress: falseis untouched.Stale prompt. Three options were available:
releaseenvironment's reviewers are a single user.actions: read, but the prompt stays waiting and keeps holding the group.actions: write.I chose cancel, limited to push-lane runs, plus the guard refusal described above. This is live right now: run 36539819278's
Publish 17.5.0 to npmjob has been waiting since 08:11:15Z, while cli@17.5.0 has been on npm since 07:58:57Z. A GET-only replay of the sweep against the live repository judges itcancel. The author cancelled nothing.Pins.
node scripts/release-pending-publish.mjs --self-testruns 32 cases across 11 batteries.actionlintis not used in this repo; the workflow was parsed withyaml@2.9.0and gives 5 jobs with no errors.Verification record (head
ad8134464d)Self-test: exit 0,
32 cases across 11 batteries pass.Ablations. The fix was committed first, each ablation went through
scripts/ablation-replace.mjs, and every restore was checked as blob equal to HEAD withgit diff HEADempty.earlieranswers pendingreturnabove the verdictreturned without reaching its verdict)not declared,DID NOT RUN)The first handshake attempt was an empty operation: the tool refused it because the replacement contained the anchor. It was redone with an anchor that drops out.
Wiring ablation: with
lint.ymlat HEAD~1,check-self-test-wiredexits 1 with[self-test-not-run] scripts/release-pending-publish.mjs. Restored, and the hash matches.Step scripts run as written. The
auditandguardssteps were extracted from the YAML and run underbash -e:audit, with npm/gh shims: the version push is pending on8c87d26a; a later landing gets a notice and is not pending; a zerobeforegets a warning and is not pending; a dispatch is pending; a published version takes the backfill path.guards, in a throwaway--sharedclone:force: passes;Gates.
dispatch-gates --commandsderived 65 families atad8134464d. 64 exited 0 and 1 is NOT MEASURED.--ranreconciled them as65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN.check:type-check-debt, PREREQUISITE NOT MET, because it needs the wholepackages/*build. This diff touches no file underpackages/. Declared to CI.check:doc-formula-expressionsneededformulaandlintbuilt first (through the verify lock, 4/4 cached); it then exited 0.check:pm-dispatch-gates: 1976 self-test cases in 972.6s, and both halves exited 0.Lint, as a declared narrowing.
eslint.config.mjsapplies 2 rules toscripts/*.mjs(from--print-config).--format json: 1 file, 0 errors, 0 warnings.parserOptions.project), so this diff cannot move a verdict on an untouched file. The other three changed files are YAML and Markdown, which the config does not lint.pnpm lintis left to CI.Changeset:
skip-changeset. All four paths sit in the private root package (.github/, rootscripts/) ordocs/adr/. No non-private package'sfiles[]names any of them, so nothing ships to npm.Acceptance notes
stale-prompts). Until then it holdsrelease-publish-refs/heads/mainand would hide the next version's prompt, the same way 17.4.0's prompt hid 17.5.0's. The maintainer can cancel it by hand before then; nothing in this PR touches it.release-github-releases.mjsreadsGITHUB_SHAfor the CHANGELOG permalink andtarget_commitish.GITHUB_*variables cannot be overridden from a step. When that step runs, the tags already exist at the version commit (release-publish.shpushed them), sotarget_commitishis unused, and the permalink points at the queuing head's CHANGELOG, which carries the version's section. Not changed, because the script is outside this card's file surface. Carrier: none.release-integritybackfill (Releases,spec-changes.json --prepare/--attachfor an already-published version) still runs ongithub.sha's workspace, not the version commit's. That is the same class (acting ongithub.sha) but it is the no-mint repair lane and outside this card's surface. Carrier: none.7510663c87, 08:05Z) is an observation, not a filed defect. Its audit found cli@17.5.0 on npm while 36536081716 was still mid-publish (the CLI is the canary; the publish finished at 08:11:15). It then took the backfill branch, and its Docker job failed at 08:09. The cause is NOT MEASURED: the job logs answer 403 through this container's proxy.release-verify-npm.mjs's header says the tree was "proved [to match]github.sha"; it is now the version commit. The file is outside the surface.scripts/adr-anchors/entry, before or after this PR. The implementing files name it in comments.publishmixes two commits: its steps come from the workflow atgithub.sha, and the scripts they run come from the version commit. This is documented in the YAML and in the ADR amendment.release-integrity's own group: if the version push's audit is evicted, recovery is now the repair dispatch only. Every later push says so in a notice.维护者速读(草稿)
改了什么
发版流程改成只由「版本提交」(合并 Version Packages PR 的那次落地)排一个待批准的发布,批准后发布的就是这个提交,而不是批准那一刻 main 的最新代码。此外,如果某个等待批准的发布,其版本已经在 npm 上,下一次 main 落地时会自动取消它。
为什么改
17.5.0 这次,版本 PR 合并之后又落地了 8 个 PR。每次落地都重新排了一个发布,并把前一个挤掉,最后被批准的那个发布的是 main 的最新代码,其中有一个破坏性变更,它的变更说明也不在 17.5.0 的 CHANGELOG 里。另外,17.4.0 留下的等待批准提示挂了 20 天,挡在 17.5.0 前面,结果被误批准。
风险与代价(含回滚)
actions: write的任务,只用来取消「版本已在 npm 上」的 push 触发的等待提示。它不取消手动 dispatch,也不取消已经开始的任务。席位意见
你要做的(一个动作)
审阅 ADR-0125 的修订,然后合并本 PR。
Generated by Claude Code