Skip to content

fix(release): queue the publish only on the push that carries the version commit, and publish that commit (ADR-0125 D1 amended) - #20625

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-20613-publish-the-version-commit
Sep 29, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-20613-publish-the-version-commit

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 triage 5886053015's direction, with ADR-0125 D1 amended in the same PR (Tier H: the maintainer merges).

  • Only the push that carries the version commit queues a publish. release-integrity now names the version commit: the newest first-parent commit at which packages/cli/package.json's version differs from its parent's, i.e. the landing that brought main to its version. On a push, publish-pending is true exactly when that commit is in this push (not an ancestor of github.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.
  • The publish job builds the version commit, never github.sha. It checks out needs.release-integrity.outputs.version-commit with fetch-depth: 0. The guard refuses an empty or mismatched ref (an empty ref: would silently fall back to github.sha), a commit that is not on origin/main as 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.
  • A stale prompt does not stay waiting. The new stale-prompts job (actions: write, contents: read) cancels a push-lane run whose Publish VERSION to npm (awaiting approval) job is waiting at release for 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, outside force.
  • scripts/release-pending-publish.mjs (new) holds the three decisions (select, npm-state, sweep), with a battery-floored, handshake-guarded --self-test wired in lint.yml.
  • ADR-0125: an Amended status 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 approving release is still the authorisation.

The dispatch's hypotheses, measured

  1. 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:

    • A merge-queue batch that carries the version commit (run 36533007563, 7a1faf1a..3a89d459) selects 8c87d26a and is pending. The five pushes after it are not pending.
    • before all zeros, before missing from the clone, or before not an ancestor (a force-push): not pending, with a notice or warning.
    • A non-linear landing selects the merge commit on the first-parent chain.
    • A dispatch is pending exactly while the version is off npm.
    • A shallow clone is refused. At the graft boundary the oldest commit has no parent, so it would read as the commit that changed the version.
  2. Every github.sha read in publish, with the sha each one now takes:

    • guard and summary: the version commit (the summary also names the head of the push that queued the run);
    • Turbo key: the version commit;
    • release-spec-changes.sh, release-verify-npm.mjs: they read the workspace, which is now the version commit;
    • release-github-releases.mjs reads GITHUB_SHA: left unchanged, see Acceptance notes.
  3. Concurrency. Measured from the Actions API, and the mechanics differ from the card's wording. The job that takes release-publish-REF then 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: false is untouched.

  4. Stale prompt. Three options were available:

    • Reject: not available to a workflow. The review endpoint answers only a required reviewer, and the release environment's reviewers are a single user.
    • Report: needs actions: read, but the prompt stays waiting and keeps holding the group.
    • Cancel: needs 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 npm job 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 it cancel. The author cancelled nothing.

  5. Pins. node scripts/release-pending-publish.mjs --self-test runs 32 cases across 11 batteries. actionlint is not used in this repo; the workflow was parsed with yaml@2.9.0 and 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 with git diff HEAD empty.

    Mutation Result
    earlier answers pending 4 red
    shallow refusal removed 1 red
    dispatch exclusion removed from the sweep 1 red
    version commit := head 6 red
    return above the verdict dispatch refuses (returned without reaching its verdict)
    battery renamed floor red twice (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.yml at HEAD~1, check-self-test-wired exits 1 with [self-test-not-run] scripts/release-pending-publish.mjs. Restored, and the hash matches.

  • Step scripts run as written. The audit and guards steps were extracted from the YAML and run under bash -e:

    • audit, with npm/gh shims: the version push is pending on 8c87d26a; a later landing gets a notice and is not pending; a zero before gets a warning and is not pending; a dispatch is pending; a published version takes the backfill path.
    • guards, in a throwaway --shared clone:
      • live approval: passes;
      • stale approval: refused;
      • force: passes;
      • empty ref: refused;
      • HEAD mismatch: refused;
      • head that is not the version commit: refused;
      • off-main version-shaped commit: refused;
      • shallow clone: refused.
  • Gates. dispatch-gates --commands derived 65 families at ad8134464d. 64 exited 0 and 1 is NOT MEASURED. --ran reconciled them as 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN.

    • NOT MEASURED: check:type-check-debt, PREREQUISITE NOT MET, because it needs the whole packages/* build. This diff touches no file under packages/. Declared to CI.
    • check:doc-formula-expressions needed formula and lint built 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.

    • Population: eslint.config.mjs applies 2 rules to scripts/*.mjs (from --print-config).
    • --format json: 1 file, 0 errors, 0 warnings.
    • The config is not type-aware (no 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.
    • The full pnpm lint is left to CI.
  • Changeset: skip-changeset. All four paths sit in the private root package (.github/, root scripts/) or docs/adr/. No non-private package's files[] names any of them, so nothing ships to npm.

Acceptance notes

  • Live stale prompt, run 36539819278. After this merges, the first push to main cancels it (stale-prompts). Until then it holds release-publish-refs/heads/main and 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.mjs reads GITHUB_SHA for the CHANGELOG permalink and target_commitish. GITHUB_* variables cannot be overridden from a step. When that step runs, the tags already exist at the version commit (release-publish.sh pushed them), so target_commitish is 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.
  • The release-integrity backfill (Releases, spec-changes.json --prepare/--attach for an already-published version) still runs on github.sha's workspace, not the version commit's. That is the same class (acting on github.sha) but it is the no-mint repair lane and outside this card's surface. Carrier: none.
  • Run 36540562567 (push 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.
  • Stale comment: 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.
  • ADR-0125 has no scripts/adr-anchors/ entry, before or after this PR. The implementing files name it in comments.
  • From the checkout on, publish mixes two commits: its steps come from the workflow at github.sha, and the scripts they run come from the version commit. This is documented in the YAML and in the ADR amendment.
  • Residual race in 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.
  • The card's operational workaround (pause the queue between merging the version PR and approving) is no longer needed once this lands. 17.5.0's aftercare is not in this PR; it is the maintainer's call.

维护者速读(草稿)

改了什么

发版流程改成只由「版本提交」(合并 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,也不取消已经开始的任务。
  • 如果版本推送那一次的审计被并发挤掉,后面的落地不会再"顺带"补排发布,需要手动 dispatch 修复通道。原来的补排本身就是这次的缺陷。
  • 合并后的第一次落地会自动取消当前挂着的 17.5.0 陈旧提示(run 36539819278)。
  • 17.5.0 已发布内容的善后不在本 PR 里。
  • 回滚方式:revert 本 PR 即可,没有数据迁移。

席位意见

你要做的(一个动作)

审阅 ADR-0125 的修订,然后合并本 PR。


Generated by Claude Code

…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
…ueues the publish, and the publish builds that commit

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ad8134464d3ed899ffca9a00c5b6c019d28b1872
Local-runs: none

Head resolved from refs/os-seat2/pr20625 and equal to the PR's head.sha. Read-only: the diff against origin/main (1322cc72c9), the card and every comment, the PR body and file list, ADR-0125 and AGENTS.md PD #15 at origin/main, the check-runs on this head (read once), and GET reads of runs 34308599522, 36533007563, 36533376091, 36533921490, 36535264066, 36536081716, 36539819278, 36540562567 and their jobs. Nothing built, run or re-run.

① Derived judgments

Public surface: none. Four files, exactly the claim's file surface. Every accept-set change is in release.yml's behaviour on push to main and on the repair dispatch, each judged from the code:

1. The predicate (release-integrity audit calling select). The version commit is the newest first-parent commit whose packages/cli/package.json version differs from its first parent's; on a push, pending iff that commit is not an ancestor of github.event.before AND the version is absent from npm; on a dispatch, iff absent from npm.

  • Merge-queue batch carrying the version commit (run 36533007563, 7a1faf1a..3a89d459): selects 8c87d26a, in-push, pending. RIGHT. Confirmed with a plain git read on origin/main: the newest first-parent commit touching the manifest is 8c87d26a (17.5.0, parent 17.4.0); the next two (c577e666, 8d1f7ab7) touch it without moving the version, which is why the dependency-only battery is load-bearing.
  • A later landing: earlier, not pending, a notice naming the repair lane. RIGHT, and it is triage's direction ("a later landing queues nothing").
  • before all zeros, absent from the clone, or not an ancestor: unreadable, not pending, a warning. RIGHT. On main only a force-push can produce it; the dispatch covers it.
  • A non-linear range: the merge on the first-parent chain, whose tree carries the version. RIGHT.
  • A bump then a revert: the head's version is the reverted one; on npm it reads published and queues nothing; off npm the revert commit is the version commit, the guard's parent check passes, and main's version ships. Consistent with "publish what main carries". RIGHT.
  • Two bumps in one push: the newest is selected; the intermediate version never gets a prompt. Same as before this PR (only main's version was ever considered). RIGHT.
  • A shallow clone: select throws, the audit exits 1, publish needs it and is skipped: fails closed. fetch-depth: 0 makes it unreachable in CI. RIGHT.
  • The two readers of one object (git show in bash and the walk) must agree or the step refuses. RIGHT.
  • version-commit is written to the outputs before the npm branch, so the force dispatch has a ref. RIGHT.
  • False negative that remains: the version push's own audit evicted from release-integrity-main (the pre-existing documented race). Recovery is now the repair dispatch only, and every later push's notice says so. RIGHT by triage's direction; the window is widened by fetch-depth: 0, see ③.
  • False positive: none found. Pending needs the version absent from npm on both lanes; unknown keeps the historical "not on npm" reading, which only yields a prompt a human still approves. RIGHT.

2. The publish job builds the version commit.

  • Checkout ref: needs.release-integrity.outputs.version-commit, fetch-depth: 0. An empty ref would fall back to github.sha; the guard's 40-hex check refuses it first. RIGHT.
  • Guard: HEAD equals VERSION_COMMIT; not shallow; ancestor of refs/remotes/origin/main as fetched at checkout, i.e. after the approval (the whole job is held); ancestor of GITHUB_SHA (the head it was selected from). All sound and fail-closed. The origin/main test relies on actions/checkout's documented fetch-depth-0 refspec (all heads and tags); the dev exercised it in a --shared clone, not on a runner.
  • 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 still holds: HEAD equals VERSION_COMMIT is asserted first, so "object database vs workspace" is the same assertion as before, now on the commit being published. Plus: the parent must carry a different version, and AUDITED must equal the committed version. RIGHT.
  • A stale prompt approved outside force: npm view present refuses; unknown warns and continues (changeset publish skips an existing version). Sound, and it is exactly the set the sweep cancels, so the read-then-cancel race against an approval is benign.
  • Every later GITHUB_SHA / github.sha read: the guard's ancestor check (intentional); the run summary's "queued by" line (informational, correct); the Turbo key now the version commit (harmless either way, turbo re-hashes); check-changeset-fixed, pnpm run build, the console dist steps, release-spec-changes.sh --prepare/--verify, release-verify-npm.mjs read the workspace, which is the version commit: RIGHT; release-github-releases.mjs reads GITHUB_SHA for the CHANGELOG permalink (the queuing head, whose CHANGELOG carries the section because the version commit is its ancestor) and target_commitish (unused: the tags already exist, pushed by release-publish.sh): harmless, not a wrong-tree defect; the docker job builds from npm (docker/Dockerfile runs npm install -g @objectstack/cli@VERSION) and its checkout at github.sha supplies only the docker/ context: harmless, pre-existing class. Tags are created by changeset publish at HEAD, the version commit, and pushed atomically; they point at a commit on main. RIGHT.
  • "Steps from the workflow at github.sha, scripts from the version commit": stated in the YAML and the ADR; a missing script fails loudly. Accepted.

3. stale-prompts.

  • Permission: job-level actions: write and contents: read; release.yml has no workflow-level permissions: block and no other job's grant moved. Scoped to that one job. RIGHT.
  • Cancel set, all conditions required: not the calling run; run status waiting; a waiting job matching Publish VERSION to npm (awaiting approval) (an unevaluated or empty name does not match); pending deployment on release; event push; npm present. in_progress runs and dispatch runs are untouched; unknown npm is kept. Can it cancel a run that could still publish something not on npm? No: a push-lane prompt whose CLI is on npm is exactly what the new guard refuses, and force is dispatch-only. A dispatch repair run? Never (event filter). A started job? Never (status filter; the sub-second approval race lands in the guard's refusal). RIGHT.
  • It cancels the run (POST runs/ID/cancel); in a waiting push run the only live job is the held publish (release-integrity completed before publish existed, docker needs publish, version-pr is skipped). RIGHT.
  • --dry-run refuses any non-GET at the HTTP layer. RIGHT.
  • Same two events as release-integrity, not needs-ed by publish: a red sweep cannot hold a release. RIGHT.

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 release-publish-refs/heads/main throughout. 17.5.0's five publish jobs were created 06:48:38 / 06:52:42 / 06:58:40 / 07:12:44 / 07:21:13; the first four were cancelled at 06:52:43 / 06:58:41 / 07:12:45 / 07:21:13, each within a second of the next's creation. That is pending jobs evicting each other behind a holder that waits at the environment, not "each landing cancels the waiting one". The last (36536081716) took the group after 07:39:36 and started 07:40:26. Run 36539819278's job (created 07:58:46) pended behind it and took the group at 08:11:15, when the publish completed; it is still waiting at read time. The dev's correction is RIGHT and the card's wording was off exactly as the dev says. The fix: one publish job per version, so nothing pends and nothing evicts; cancel-in-progress: false is untouched; the sweep never touches a non-waiting run. "Never cancel a publish mid-flight" holds.

5. ADR-0125 amendment. Status line · Amended (date, #N, see the section at the end), inline Amended blockquotes under D1 and D5, and ## Amendment (2026-09-29, #20613): ... appended: the idiom of ADR-0005 (status plus trailing section) and of 0087 / 0094 / 0126 (inline blockquote). The corrected premise ("true until the publish finishes") and the run table match the runs above. D2, D3, D4 and D6 are named unchanged; D1's original text stays as the record; the diff touches nothing else in the file. RIGHT. Nit: the D1′ / D1″ prime marks have no precedent in docs/adr/; harmless naming.

6. lint.yml. One step, Release version-commit selection self-test, in the lint job (Lint & Repo Gates), directly after release-verify-npm.mjs --self-test, invoked as node per the file's idiom; +16/-0, nothing else moved. check:self-test-wired requires it because release.yml runs the script. RIGHT.

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

skip-changeset: RIGHT. The four paths are .github/workflows/*, root scripts/* (root package @objectstack/spec-monorepo, "private": true) and docs/adr/*; nothing under packages/, and no released package's files[] ships them. Check Changeset on this head: success.
Clause-②: no. RIGHT: no contract accept or reject behaviour moves; nothing publishes.

③ Boundary flags

  • Dev open_questions (A cancel / B report-only / C guard only): ANSWERED A, on the code (the cancel set can publish nothing, never touches a dispatch run or a started job, and is what the guard would refuse anyway) and on triage's direction ("cancelled, not left waiting"); B and C leave the holder hiding the next real prompt, which is the measured 17.4.0-to-17.5.0 failure. ESCALATED as a permission-boundary line the Tier H quick-read must carry regardless: (a) actions: write on GITHUB_TOKEN inside the release workflow also permits POST workflows/ID/dispatches, which for that token does create a run, so the job's token could queue a repair-lane prompt at the release gate; the script never sends it, --dry-run refuses non-GET structurally, and no publish can follow without the environment approval (D3); (b) the sweep is an automated cancel of a pending release deployment, an act PD Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15 does not list but sits next to ("a deployment waiting for hours is the system working, not a state you clear"); it clears only prompts that can publish nothing, and the maintainer's merge is the authorisation. The 速读 draft already names the grant and the first cancel (36539819278); it should also carry sentence (a).
  • Deviation, hypothesis-3 correction: verified on the runs (① item 4). Accepted.
  • Deviation, skip-changeset in the one label call: right by AGENTS.md's criterion.
  • Deviation, the guard inlines the npm reading: right; the guard executes nothing from the tree it guards, and the version commit may predate the script.
  • Deviation, model-free trailers (Co-authored-by: Claude plus Claude-Session) on all four commits: AGENTS.md's form; right.
  • Deviation, check:type-check-debt NOT MEASURED locally: Type Check · debt ledger on this head is success. Answered by CI.
  • Out-of-scope, release-github-releases.mjs reads GITHUB_SHA: judged harmless in ① item 2; carrier none is right.
  • Out-of-scope, the backfill lane acts on github.sha: pre-existing, no-mint; carrier none is acceptable.
  • Out-of-scope, run 36540562567 (docker backfill red at 08:09:38 after the 08:05 audit read cli@17.5.0 present while 36536081716 was still publishing until 08:11:15): the job log is unreadable from here as well (the log redirect's blob host is denied by the proxy). The timeline is itself the repro: the backfill lane reads "cli on npm" as "publish complete". ESCALATED to the seat: file it as a finding (PD chore: version packages #10); outside this card's surface and not this diff's.
  • Out-of-scope comment drift in release-verify-npm.mjs and the missing scripts/adr-anchors/ entry for ADR-0125: noted, not this diff's.
  • Reviewer's flags: (1) fetch-depth: 0 on release-integrity runs on every push to main and lengthens the audit; the eviction window of release-integrity-main is the audit's duration, and recovery is now dispatch-only, so the window is widened, NOT MEASURED. Seat: read the first post-merge push's audit duration; if it is minutes, filter: blob:none with fetch-depth: 0 is the cheap fix (the walk needs commits, and blobs for one path). (2) Merge-queue granularity: the version commit's tree includes batch entries that landed before it in the same batch, whose changesets are unconsumed; one batch instead of eight PRs, inherent to the queue and stated in the ADR ("one merge-queue batch apart"). (3) A waiting run created before the merge executes the OLD workflow file if approved; the sweep removes only those whose version is on npm. Transitional, and the PR body says so for 36539819278.
  • AGENTS.md PD Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15: nothing in the diff publishes, tags, cuts a Release, pushes an image, dispatches release.yml, approves a deployment, or touches the Version Packages PR; the dev cancelled nothing (36539819278 still waiting at read). Merging this PR is a push to main whose audit finds 17.5.0 on npm, so publish is skipped and the sweep cancels that stale prompt. Nothing makes a release act easier to walk by accident beyond the escalated grant. The PR is draft, no auto-merge, no reviews: Tier H, the maintainer's hand.
  • Gate coverage (check-runs on this head, read once at judging): green: TypeScript Type Check, Type Check source / workspace / debt ledger / consumer gates, Test Core 2/6 to 6/6, Dogfood Regression Gate, Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, the claim / single-writer / part-of guards. NOT CONCLUDED: Lint & Repo Gates (109350439284, in_progress; it carries the new self-test, check:self-test-wired, adr-links, doc-authoring, governed-prose and eslint on the new script) and Test Core (1/6) (109350551209, in_progress). Skipped by path filter: Build Core, Temporal Conformance, Console Pin Gate, Build Docs, dogfood shards, tarball smoke. No red. No check-run exercises release.yml itself (it runs on push to main only): the YAML plumbing is judged by reading and by the dev's extracted-step runs, the script's logic by its self-test, whose CI verdict is the not-yet-concluded Lint job. The landing waits for those two.

Implemented-by: claude/issue-20613-publish-the-version-commit
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

domain:spec seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T10:12Z · 终稿:席位对照自己读过的 diff 校正了 dev 的草稿并填写席位意见。复核记录:#20613 上的 ACCEPT 5888090391,本 PR 上的达档复审 5888029963(PASS)。

改了什么

  • 发版流程只在「版本提交」(合并 Version Packages PR 的那次落地)上排一个待批准的发布;批准后发布的就是这个提交,不再是批准那一刻 main 的最新代码。
  • 发布任务的守卫会拒绝:版本提交对不上、提交不在 main 上、以及已经发布过的版本的过期提示(force 修复通道除外)。
  • 新增一个 stale-prompts 小任务:如果某个等待批准的发布,其版本已经在 npm 上,下一次 main 落地时自动取消它。
  • 逻辑和 32 个自测用例在 scripts/release-pending-publish.mjs,接进 lint.yml;ADR-0125 增补一段修正 D1 的前提。

为什么改

  • 17.5.0:版本 PR 合并后又落地了 8 个 PR,每次落地都重新排发布、挤掉前一个;最后被批准的那个发布的是 main 的最新代码,其中一个破坏性变更(92fe0814)没有写进 17.5.0 的 CHANGELOG。
  • 17.4.0 留下的等待提示挂了 20 天,一直占着发布并发组,把 17.5.0 的真提示挡在后面,结果被误批准。
  • dev 用 17.5.0 真实的推送历史回放:只有带版本提交的那次推送会排发布,之后 4 次落地都不会。

风险与代价(含回滚)

  • 权限边界(需要你知情): stale-prompts 任务持有 actions: write。它只取消 push 触发、正在等 release 审批、且版本已在 npm 上的运行,不碰手动 dispatch 的修复通道,也不碰已经开始的任务。复审补充一点:这个令牌的 actions: write 在权限上也允许触发 workflow dispatch;脚本从不这样做,而且没有你的环境审批,任何发布都走不下去。自动取消一个待审的 release 部署,是 AGENTS.md 第 15 条旁边的动作:它只清掉「什么都发布不了」的提示,授权来自你合并本 PR。
  • 现在就挂着一个: run 36539819278「Publish 17.5.0 to npm」从 08:11Z 起在等审批,而 17.5.0 在 07:58Z 已经在 npm 上。它占着发布组,会挡住 17.6.0 的提示。本 PR 合并后的第一次落地会自动取消它;在那之前,席位不拒绝也不取消它(第 15 条),请你手动 Reject,或等本 PR 合并。
  • 如果版本推送那一次的审计被并发挤掉,后面的落地不会再顺带补排发布,需要手动 dispatch 修复通道。以前的「顺带补排」正是这次的缺陷。
  • release-integrity 现在每次 main 落地都拉完整历史,审计会变慢,未测量。席位会读合并后第一次推送的审计耗时,如果到了分钟级,改 filter: blob:none 就行。
  • 17.5.0 已发布内容的善后不在本 PR 里。另一个相关缺陷已立卡 [finding] release.yml: release-integrity reads "cli on npm" as "the publish is complete", so a landing during a publish starts the docker backfill before the fixed group is on npm, and it goes red #20627:发布进行中落地时,Docker 回填会提前开跑并变红。
  • 回滚:revert 本 PR 即可,没有数据迁移。只想去掉自动取消的话,删掉 stale-prompts 这一个 job 就退回到「只靠守卫拒绝」。

席位意见

  • 建议合并。 达档复审 PASS:谓词的各种边界(批量合并、之后的落地、before 为零、非线性历史、先升后回滚、一次推两个版本、浅克隆)都从代码核对过,并对照了 main 的真实历史;发布任务里其余读 GITHUB_SHA 的地方都无害;「发布中途永不取消」的保证不变;diff 里没有任何执行、触发或批准发布的动作。
  • 自动取消,建议保留(A)。 这是分诊给的方向(「取消,而不是留着」),复审认为代码上安全。另外两个选项的代价是实测过的:B 只报告,C 只靠守卫拒绝,两者都会让过期提示继续占着发布组、挡住下一个版本的真提示,也就是 17.4.0 挡 17.5.0 的原样。如果你不想给这个权限,在 PR 上留一句 B 或 C,席位让 dev 改一个 hunk。
  • 席位核过 dev 与复审者的转录:两者都没有调用任何 Actions 写接口。

你要做的(一个动作)

审阅 ADR-0125 的增补后合并本 PR;或者提交 APPROVED 审批,由席位跑落地前检查后入队。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants