Skip to content

fix(release): refuse a Version Packages PR whose queue build lands behind main; mark next.2's 11 missed changesets - #23833

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23818-version-pr-behind-main
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23818-version-pr-behind-main

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23818

Clause-②: no

What this PR does

Each Version Packages cut now records, by itself, what the previous cut shipped without a CHANGELOG entry. It never refuses or holds a release.

  • New mode annotate-shipped in scripts/release-pending-publish.mjs.

    • It finds the last version commit and the changesets that sat unconsumed in its tree.

    • Each one still pending gets this lead line:

      **Shipped in VERSION**, whose CHANGELOG missed this entry; listed here one prerelease late.

    • changeset version then renders that line as the entry's first line under the next version.

  • One package.json line. The root version script runs the step first, joined with ; so it cannot stop the version pass:

    node scripts/release-pending-publish.mjs annotate-shipped; changeset version && ...

    Every caller of pnpm run version gets it with no workflow edit.

  • The merge-queue refusal is withdrawn. The queue-guard mode, its batteries and the lint.yml step are gone, and lint.yml has no diff against main. This PR is not governed: check-governed-merges --branch reads 0 of 2 paths.

  • The 11 next.2 entries already landed in docs(changeset): mark the 11 changesets 18.0.0-next.2 shipped without a CHANGELOG entry #23835 (3c080a65c). This branch merged main, so its diff is 2 files: package.json (+1 / -1) and the script (+501 / -26). That is 529 changed lines.

Root cause: why #21373's guard did not catch next.2

#21373 added release-pending-publish.mjs unconsumed. release.yml's release-integrity job runs it in the push that carries the version commit.

  • It lists every top-level .changeset/*.md in that commit's tree that pre.json does not record as consumed.
  • It reports them as a ::warning:: and a job-summary section.
  • Its contract: "The report never refuses a release."

On next.2 it measured correctly. Run 38841989023 (the push of e5136ff10f, 02:44Z) carried the warning 11 changeset(s) from 10 commit(s) are still in the tree of the version commit e5136ff10f .... The publish job started at 02:47:48Z. The miss was about timing, not pre mode:

  • It reads after the landing. By then the CHANGELOG files are sealed into the version commit the publish ships. It only warns, and nothing acts on the warning.
  • Pre mode played no part. @changesets/cli v3 moves what a prerelease pass consumes into .changeset/pre/, and pre.json lists no ids, so the top-level read is right in pre mode.

The card's premises, checked in a full-history clone:

  • Ancestry. Each of the 11 add commits is an ancestor of e5136ff10f (exit 0) and not of the PR's base 4fe8b3def0 (exit 1). Control leg: root against 4fe8b3def0 gives exit 0.
  • Not consumed. None of the 11 is under .changeset/pre/ at e5136ff10f.
  • No CHANGELOG entry. No ## 18.0.0-next.2 section names any of them.

Why not refuse the landing

The maintainer asked, verbatim: 「23833 做的对吗?发版本的时候 main 上的队列就是不停在合并的」

The seat measured it (record 6211287288 on this PR):

  • In the 24 h to 2026-10-16T08:00Z, main took 111 first-parent landings, and 77 of them added a changeset: about 3.2 an hour, with at least one in every hour.
  • Queue time runs 18 to 33 minutes.
  • So a refreshed Version Packages PR would get through a refusing guard about one time in ten, and the guard would turn a mis-dated CHANGELOG line into a release blocker.

The detection was right; refusing was the wrong remedy. This PR keeps the detection and moves the remedy to the next cut.

The annotate step

  1. Find the last version commit on HEAD's first-parent chain, by select's definition: the CLI version differs from the first parent's.
  2. Take the changesets left unconsumed there, the same set unconsumed reports. They are read from that commit's tree alone (pendingChangesetsAt, which unconsumed now shares), so the step needs history only back to that commit's parent.
  3. Annotate each one still pending. The lead line goes right after the frontmatter, and every other byte is unchanged.
  4. It never blocks.
    • The mode exits 0 on every path, and the ; keeps even a crash from stopping changeset version.
    • When it cannot decide, it prints a ::warning:: and writes nothing. That covers no version commit, unreachable history, an unreadable tree or pre.json, and npm unreadable.
    • A changeset it cannot parse is skipped with its own warning.
  5. A shallow clone deepens itself, with bounds. It runs git fetch --no-tags --deepen=N origin HEAD-SHA with N = 100, 200, 400, 800, 1600, until the version commit and its parent are both present. It stops early, with a warning, when a round gains nothing. --no-deepen turns the fetch off.

Proof

Self-test. node scripts/release-pending-publish.mjs --self-test passes 112 cases across 28 batteries. The six annotate batteries:

  • the next.2 shape (v3 pre-mode layout): exactly the 3 late changesets are annotated, with byte-exact output. A changeset added after the version commit and one consumed into pre/ are untouched. A CRLF changeset keeps its line endings, and a changeset with no fence is unparseable;
  • a second run writes nothing, and every file stays byte-identical;
  • a hand-annotated changeset (docs(changeset): mark the 11 changesets 18.0.0-next.2 shipped without a CHANGELOG entry #23835's form, card number included) is left byte-identical, and the other two are annotated once each, read from disk;
  • non-pre mode (17.6.0 -> 17.7.0) uses the "release" form;
  • a depth-1 clone deepens itself and annotates the same three, read from disk. With origin unreachable it warns, writes nothing, and the CLI exits 0. With --no-deepen it is undecided;
  • the no-decision cases: no CLI manifest; a manifest born in the root commit; a manifest created later; a version not on npm (a plain line); npm unreadable; a run outside any git repository (the CLI exits 0 with a warning); an unreadable pre.json. Nothing is written in any of them.

Ablation. scripts/ablation-replace.mjs replaced the write writeFileSync(w.abs, w.next); with void w.next; (anchor 1 -> 0, blob 4bfd5209005c -> 9566045703cc). The self-test went red on 5 cases: byte-exact, second run, hand-annotated, non-pre, depth-1. Restore: the blob equals HEAD and git diff HEAD is empty.

Real history. The script is this branch's. Each clone below was made the way actions/checkout makes the version-pr job's (depth 1, sha-pinned) unless stated otherwise.

tree result
origin/main 3fed1eb00, before #23835 landed deepened by 100 commits in 6.9 s; last version commit e5136ff10f (next.1 -> next.2); exactly the 11 annotated; exit 0
same tree, second run 0 annotated, 11 already; tree byte-identical to the first run's
the same 11 compared with #23835's blobs each differs only in that one line, by the (#23818) card number
#23835's head af52476f3 0 annotated, 11 already; nothing written
3c080a65c (#23835 landed; the base of the seat's 08:45Z re-cut of #23825, run 38875444505) 0 annotated, 11 already; nothing written
the re-cut #23825 head dedc6fe388 last version commit is the re-cut itself (next.3, not on npm), so "nothing shipped without an entry"; nothing written

The CHANGELOG renders the line once.

Every caller of pnpm run version, measured

caller checkout what the step does there evidence
release.yml version-pr job, render step depth 1 (default) deepens the clone itself, then annotates the depth-1 clone above (+100 commits, 6.9 s, exit 0). Moved paths with the step: 230; without (the old script on the same tree): 230, identical sets, so the SHAPE validation sees nothing new. All 10 content gates that step runs exit 0 on the annotated tree: check:docs-image-tag, check:docs-image-tag-sync, check:template-version-sync, check:release-index-currency-sync, check:skill-compatibility, check:nul-bytes, check:release-notes, check:release-page-status, check-release-section-coverage.mjs, check:published-readme-links
release.yml restore step, then changesets/action version-script (same job) the deepened clone, after git reset --hard annotates again on the fresh tree, with no second fetch; this is the tree the PR commits the restore step's git checkout -- . puts the annotated files back with everything else; the 230-path set above is unchanged
cut-rc.yml fetch-depth: 0 no fetch; annotates its changeset gates run before pnpm run version; afterwards it stages .changeset wholesale, so the annotated files move to pre/ with the rest
local rehearsal (docs/releases-maintenance.md) a clone of the shared checkout exit 0, wrote only .changeset/ files measured: it found next.1's version commit and annotated next.1's 6 leaked changesets (see Acceptance notes)

What to do about #23825 (next.3) now

#23825 was re-cut at 08:45Z from 3c080a65c, so it carries the 11 entries correctly. main has moved since (66d456d01, #23840), so as cut it is behind again.

Land this PR before the next.4 cut, then merge #23825 whenever the maintainer chooses. Whatever #23825 ships unlisted when it lands, the next cut annotates as "Shipped in 18.0.0-next.3". Nothing needs freezing or refreshing at merge time.

Acceptance notes

None of these is filed. The seat decides at landing.

  • next.1 leaked too. Version commit 0c52e43075 (chore: version packages (next) #23382) left 6 changesets unconsumed: 23268-external-sharing-model-retired, 23268-skills-lint-rules-row, 23357-page-component-view-data-api-refused, 23387-two-master-names-release, 23397-ceremony-builtin-positions, 23399-ceremony-not-declared-column-drop. They shipped in next.1 and are listed in the published next.2 sections with no note: 0 next.1-section hits for each add commit. This is the same family as this card. This step covers every cut from here on; a published entry is corrected only by a docs-only amendment.
  • The GA CHANGELOG will repeat the note. At pre exit, the GA CHANGELOG re-lists every prerelease changeset, so an annotated one carries "listed here one prerelease late" into the GA section too. The docs(changeset): mark the 11 changesets 18.0.0-next.2 shipped without a CHANGELOG entry #23835 lines do the same.
  • The ADR-0087 pass line. check-adr-0087-registration's pass line counts inherited-breaking changesets as "non-breaking". Its inheritance exemption also does not re-ask an edited, already-breaking changeset for its disposition. This is read off the code.
  • The post-landing warning's remedy. unconsumed's post-landing warning still says "Name them in the VERSION release notes". With this step, the next cut does that.

Local gates (final head 1cdd9d114, merge base 66d456d01)

  • Derivation. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 38 families. Each was run with its exit code captured before any pipe. All 38 exit 0. These include:
    • release-pending-publish.mjs --self-test;
    • check-self-test-wired and check-self-test-workflow-commands;
    • check:merge-driver and check:turbo-task-graph;
    • check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure and check:sourcemap-no-sources-content, after a closure build under the verify lock (74/74 tasks, VERDICT command-exit 0);
    • pnpm check:pm-dispatch-gates (fast tier).
  • Reconciliation: --ran reports 38 derived, 38 run, 0 NOT-MEASURED.
  • Lint: eslint --no-inline-config scripts/release-pending-publish.mjs exits 0.
  • Governance: check-governed-merges --branch reports 0 of 2 paths governed, 529 changed lines (+502 / -27). lint.yml has no diff against origin/main.

Generated by Claude Code

…hind main

`release-pending-publish.mjs queue-guard` judges a merge-queue build: it
finds each version commit on the first-parent chain of base..head and takes
the `unconsumed` measurement AT that commit, refusing (exit 1) when any
changeset is left over -- the PR was cut from a base that main, or a queue
entry ahead of it, has since passed by a changeset. The existing `unconsumed`
report reads the same thing only after the landing and only warns; the
18.0.0-next.2 push run carried that warning for all 11 changesets.

Self-test: six new batteries (28 total, 111 cases), including the next.2
shape in v3 pre mode, the refreshed-cut control, an ordinary build, a batch,
the non-pre shape, and the refusals for a missing/foreign base and a shallow
clone.

Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t
Co-authored-by: Claude <noreply@anthropic.com>
… a CHANGELOG entry

The Version Packages PR for 18.0.0-next.2 landed behind main, so these 11
changesets' code shipped in next.2 while they stayed unconsumed. Each body
gains one lead line saying so; the next Version Packages PR lists them under
next.3 with that line as the entry's first line. Frontmatter (packages and
bump types) is unchanged, and each one's breaking classification under the
ADR-0087 gate is unchanged.

Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t
Co-authored-by: Claude <noreply@anthropic.com>
… pull_request build

Measured: a scheduled refresh starts no pull_request build (#23825's head
carries zero check runs), and a dispatched one does (#23481's head carries
45), but either way that build judges the PR against the base it was cut
from. Only the merge-queue build sees the tree that lands.

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

Copy link
Copy Markdown
Contributor Author

席位核对 · PATCH ROUND · PR #23833 (draft, head 1115d438b, closes #23818) · domain:devx seat 2 · session_016PVjjuVPte5XEJvaQx1E4t · 2026-10-16T07:41Z

Round 1 is accepted as far as it goes. The guard refuses nothing until it is wired, and the wiring is this round.


Generated by Claude Code

One step in the `Lint · CI, release & engine gates` lane runs
`release-pending-publish.mjs queue-guard` with the event name and the
merge group's base and head shas from the event payload. On merge_group it
refuses a version commit that would land a changeset it did not consume;
on every other event it says "not a merge-queue build" and exits 0. The
lane verdict reads the step's outcome from its roster (continue-on-error +
id), so no roster edit is needed.

The guard now takes the head from the payload too: a merge_group build with
no full head sha is refused rather than read off the checkout. Self-test:
113 cases across 28 batteries.

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

Copy link
Copy Markdown
Contributor Author

维护者速读 · #23833 · domain:devx seat 2 · 2026-10-16T08:13Z

席位核对 · ACCEPT · PR #23833 (draft, head 31e38ea07, Fixes #23818) · domain:devx seat 2 · session_016PVjjuVPte5XEJvaQx1E4t

Checked on GitHub and against the sources, not on the report's say-so. Round 1 was accepted in the patch-round record 6210600098. This record covers the patch round and the whole PR.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

⛔ Approval request withdrawn: do not approve this head · domain:devx seat 2 · session_016PVjjuVPte5XEJvaQx1E4t · 2026-10-16T08:22Z

The seat's ACCEPT 6211127283 is withdrawn. The guard detects correctly, but refusing the queue build is the wrong remedy for how this repo's main lands.

The maintainer asked, verbatim: 「23833 做的对吗?发版本的时候 main 上的队列就是不停在合并的」

The seat measured it at this act:

What replaces it (the next round on #23818). Instead of refusing, each new cut records the leak itself, with no human step:

That is the maintainer's route A, automated, and it needs no workflow edit. The lint.yml step is dropped from this PR, which then stops being Tier H, and needs-user-decision comes off when the reworked head lands here.

#23835 is unaffected and stays in the queue. Its 11 lines are what the automated step would write.


Generated by Claude Code

…ed, instead of refusing the queue

Withdraws the merge-queue refusal. main takes a changeset-bearing landing
about every twenty minutes (77 of 111 landings in a day), so a Version
Packages PR almost never reaches the queue current, and refusing it would
block nearly every release to protect a CHANGELOG line.

`release-pending-publish.mjs annotate-shipped` runs first in the root
`version` script (joined with `;`, so it cannot stop the pass). It finds the
last version commit, takes the changesets that sat unconsumed in its tree,
and gives each one still pending the lead line
`**Shipped in VERSION**, whose CHANGELOG missed this entry; listed here one
prerelease late.` -- frontmatter byte-unchanged, idempotent (a body already
led by `**Shipped in` is left as it is), and only when VERSION is on npm. A
shallow clone is deepened in bounded rounds; anything it cannot decide is a
::warning:: with nothing written, and it exits 0 on every path.

The queue-guard mode, its batteries and the lint.yml step are removed;
lint.yml is back to its merge-base bytes. Self-test: 112 cases across 28
batteries.

Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Oct 16, 2026
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Oct 16, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #23833 (draft, head 1cdd9d114, Fixes #23818) · domain:devx seat 2 · session_016PVjjuVPte5XEJvaQx1E4t · 2026-10-16T09:24Z

Checked on GitHub and against the sources, not on the report's say-so. This record replaces the withdrawn ACCEPT 6211127283, and covers the redesign the seat ordered in 6211287288.

  • Shape:
    • 2 files, +502 / −27, inside the revised claim's surface (6211304647 on release(next): 18.0.0-next.2 shipped 11 changesets with no CHANGELOG entry (the version PR merged behind main; #21361's class, in pre mode) #23818).
    • package.json: one line. The root version script now starts with node scripts/release-pending-publish.mjs annotate-shipped; and everything after it is unchanged. The root package is private: true.
    • scripts/release-pending-publish.mjs: annotate-shipped is added, and queue-guard is removed with its batteries.
    • .github/workflows/lint.yml has no diff against origin/main.
    • Not governed: this seat ran check-governed-merges --pr 23833 and it reads 0 of 2 paths. So needs-user-decision is removed in this act, and this PR needs no approval. The review requests to os-zhuang and hotlong from the withdrawn round are moot.
    • The body's only closing keyword is Fixes #23818. Clause-②: no is at line start.
    • The seat wrote the body in this act from the dev's replacement, per os-dev.md:64. It is 12580 bytes, sha256 2930ef81b827…, and the read-back is identical.
  • The step, read by this seat on the source:
    • locateLastVersionCommit walks the first-parent chain over the CLI manifest, using select's definition. In a shallow clone it answers need-history, and annotateShipped deepens with git fetch --no-tags --deepen=N origin <sha> for N from 100 to 1600. It stops as soon as a round gains nothing.
    • It acts only when that version is on npm. If the version is absent, it writes nothing. If npm cannot answer, it warns and writes nothing.
    • It annotates pendingChangesetsAt(versionCommit).unrecorded, the set unconsumed reports, and skips files that no longer exist.
    • withLeadLine keeps the frontmatter fence byte for byte, follows the file's line ending, and treats a first body line starting **Shipped in as already annotated. That covers docs(changeset): mark the 11 changesets 18.0.0-next.2 shipped without a CHANGELOG entry #23835's 11.
    • A failed write puts back the files it already wrote. Any exception ends in undecided with a warning, and the CLI exits 0. The ; in the version script means even a crash cannot stop changeset version.
  • Measured by the dev:
  • Gates:
    • Local: 38 derived families, 38 exit 0, --ran 0 NOT-MEASURED.
    • CI on 1cdd9d114: 33 success, 6 roster skips, and 1 failure, Check Changeset ("adds no changeset"). The seat applied skip-changeset in this act: scripts/** and the private root package.json ship in no package.
  • Out of scope, decided by this seat:
    • next.1 leaked too. Six changesets that shipped in next.1 are listed under next.2 with no note. This is the same family, so it is not filed. Every cut from here on is covered, and a published section changes only by a docs amendment, which is the release owner's call.
    • The GA note. At pre exit, the GA CHANGELOG repeats the lead line. That is accurate, and it is not filed.
    • Two check-adr-0087-registration observations. One is wording; the other is read off the code and not exercised. Neither is filed.
  • Landing: once Check Changeset reads green on this head, the seat readies the PR, arms auto-merge, and confirms the merge on origin/main.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 09:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 09:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 0af559e Oct 16, 2026
54 of 55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23818-version-pr-behind-main branch October 16, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

release(next): 18.0.0-next.2 shipped 11 changesets with no CHANGELOG entry (the version PR merged behind main; #21361's class, in pre mode)

3 participants