Skip to content

chore(release): migrate the release toolchain to @changesets/cli v3 - #9560

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-9498-changesets-v3-migration
Aug 18, 2026
Merged

chore(release): migrate the release toolchain to @changesets/cli v3#9560
os-project-manager merged 2 commits into
mainfrom
claude/issue-9498-changesets-v3-migration

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#9498

@changesets/cli 2.31.1 → 3.0.0, with every consequential change in the same PR — epic #9465's ruling 1: both half-states are broken, and each is broken silently until the next release. Sub-issue 2/4.

The scope is #9497's rehearsal. Every number below was re-measured on this branch's tree rather than inherited, because the stock has grown from the rehearsal's 165 changesets to 209 — and one of those new changesets turns out to matter (see "Where measurement diverged from the rehearsal").


1. The next train is 17.1.0 — shown, not asserted

Maintainer ruling on #9498, verbatim: 接受 17.1.0. Proof on the migrated tree, @changesets/cli 3.0.0:

$ changeset status --output=status-v3-ga.json # non-pre, whole stock
releases by type: {"minor":69,"patch":7,"none":1}
changesets in plan: 209
@objectstack/spec 17.0.0 -> 17.1.0 (minor)
@objectstack/cli 17.0.0 -> 17.1.0 (minor)
@objectstack/dogfood 0.0.40 -> 0.0.41 (patch)
@objectstack/example-crm 4.0.92 -> 4.0.93 (patch)
@objectstack/docs 4.2.1 -> 4.2.1 (none)
major releases: NONE

and in pre mode, which is the number the next cut dispatches:

$ changeset pre enter rc && changeset status --output=status-v3-rc.json
releases by type: {"minor":69,"patch":7,"none":1}
@objectstack/spec 17.0.0 -> 17.1.0-rc.0 (minor)

Driven end to end through a full window in a throwaway clone (pre enter → RC1 → author a changeset → RC2 → pre exit → GA), all on this branch's tree:

steproot .changeset/*.md.changeset/pre/packages/cli version
pre enter rc20917.0.0
pnpm run version (RC1)020917.1.0-rc.0
author one changeset1209
pnpm run version (RC2)021017.1.0-rc.1
changeset pre exit0210(pre.jsonmode"exit", one line, nothing else)
pnpm run version (GA)0017.1.0

No changeset declares a major (major releases: NONE), and none was written to preserve 18.0.0 — explicitly rejected in the ruling. v2's 18.0.0 came from one optional peer edge (@objectstack/cli@objectstack/driver-turso) crossed with "a prerelease never satisfies a non-prerelease range"; that is traced by ablation on #9465, not re-derived here.

2. What changed, and which unknown drove it

filechangedriver
package.json + pnpm-lock.yaml@changesets/cli^2.31.1^3.0.0the bump itself
.changeset/config.jsonadd"privatePackages": {"version": true, "tag": false}U4
.changeset/config.jsonremove___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCHU2
.changeset/config.json$schema@changesets/config@4.0.0 (the version v3 resolves)maintenance line
.github/workflows/cut-rc.ymlprose only — the measured file set, and the falsified "consumed changesets are recorded in pre.json / a cut never races a lane PR" claimU3
.github/workflows/pr-automation.ymlprose only — two blocks resting on v2's "pre mode RETAINS every consumed .md"U3, U5
docs/releases-maintenance.mdprose only — same falsified pre.json claimU3
scripts/check-changeset-no-major.mjsheader only — the pollution narrative gets its v3 numbers; the stale claim that both siblings pass --diff-filter=AM is correctedU5
scripts/check-adr-0087-registration.mjscode.changeset/pre/ excluded from --list / --audit-stock, plus PRE1–PRE3 fixturesrehearsal's scope verdict

.changeset/config.json — the toggle matrix, measured here

Same 209 changesets, v3, changeset status:

config stateplan
as shipped (privatePackages set, experimental key removed){"minor":69,"patch":7,"none":1}, 77 packages, spec 17.1.0
+ experimental key restored (true)identical
+ experimental key falseidentical
privatePackages unset (v3's default)exit 1 — see below

The experimental key is measurably inert under v3 (the peer-escalation branch is gone from @changesets/assemble-release-plan@7), so dropping it is a no-op here — and it may only be dropped in this commit, because under v2 it is load-bearing on the GA path (U2).

3. Where measurement diverged from the rehearsal — and the divergence matters

#9497 measured privatePackages unset as lossy but survivable: 8 private packages stop being versioned, 2 private-only changesets never get consumed. On today's stock it is fatal:

$ changeset status # privatePackages unset, v3.0.0
Error: Found mixed changeset default-timeout-margin-repair
Found ignored packages: @objectstack/dogfood
Found not ignored packages: @objectstack/types
Mixed changesets that contain both ignored and not ignored packages are not allowed
at getRelevantChangesets (@changesets/assemble-release-plan@7.0.0/dist/index.mjs:326)
🦋 Exited with code 1

With privatePackages.version === false, v3 treats private packages as ignored, and a changeset naming one private and one public package becomes a "mixed changeset", which changesets refuses outright. Today's stock has exactly one — .changeset/default-timeout-margin-repair.md (@objectstack/types patch + @objectstack/dogfood patch), landed after the rehearsal's snapshot — plus 4 private-only changesets where the rehearsal saw 2.

So U4's recommendation is not a nicety about version numbers: without that key the bump alone takes the release lane from "wrong versions" to "will not run". It is more evidence for ruling 1, arriving from a direction nobody planned.

4. The measured v3 file set vs the enforced allowlist (U3)

git status --porcelain after changeset pre enter rc + pnpm run version, v3.0.0, 209 pending changesets — 366 lines:

=== BY STATUS CODE === === BY PATH CLASS ===
209 D 209 D .changeset/NAME.md
155 M 76 M PKG/package.json
2 ?? 76 M PKG/CHANGELOG.md
3 M the 3 declared doc surfaces
1 ?? .changeset/pre.json
1 ?? .changeset/pre/

.changeset/pre/account-oauth-tokens-internal.md diffs identical to the original — the files are moved verbatim.

the allowlist comment's claim (written under v2)v3, measured
77 modified package.json76 — the 77th was the blank template's, rewritten only at a major boundary; 17.0.0 → 17.1.0-rc.0 is not one
76 modified CHANGELOG.md76 ✅
.changeset/pre.json modifiednever modified — v3's pre.json is {"mode","tag"} and stays that way
zero deletions209
zero untracked❌ 1 new directory, .changeset/pre/, holding the 209
"a cut never races a lane PR over a .changeset/*.md file"❌ false — the cut moves 209 tracked changeset paths

The enforcement itself needs no change, replayed verbatim from the workflow against the v3 output:

doc surfaces declared by SURFACES (3): docker/README.md, content/docs/deployment/self-hosting.mdx, content/docs/upgrading.mdx
STAGED count: 365
RESULT: allowlist regex accepts every staged path
UNSTAGED ASSERTION: clean (nothing tracked left behind)
--- what got staged, by class ---
209 .changeset/pre/NAME.md
1 .changeset/pre.json
76 PKG/CHANGELOG.md
76 PKG/package.json
3 the declared doc surfaces

^\.changeset/ already covers .changeset/pre/…, and git add -A -- … .changeset … stages the deletions and the new directory together. The SURFACE_LIST / grep -vxF block from 955ccf2 is untouched — this PR changes prose in that file and one echo string, nothing else. Ruling 3 ("the allowlist follows the measurement") is satisfied by the allowlist needing no move.

5. Gate exposure, measured on a real v3 cut commit

git ls-tree -r --name-only CUT -- .changeset at the rehearsal cut:

 1 .changeset/*.md (README.md)
1 .changeset/config.json
1 .changeset/pre.json
209 .changeset/pre/*

scripts/check-adr-0087-registration.mjs's changesetDirAt uses exactly that recursive ls-tree, so from the first v3 cut the entire consumed stock of an open window flows into --list and --audit-stock — surfaces whose subject is the pending backlog. Fixed by excluding the pre/ prefix in changesetsAtonly:

Both halves are pinned and both ablations were run:

ablate the filter (changesets: entries.filter(isChangesetFile))
-> PRE1 RED (got [".changeset/pre/consumed-one.md", ".changeset/pre/consumed-two.md", ".changeset/stock-breaking.md"])
-> PRE3 RED (pending stock got [".changeset/pre/consumed.md"], expected 0)
-> PRE2 stays GREEN — it does not depend on the filter, by design
move the filter into isChangesetFile (i.e. into the VERDICT path)
-> PRE2 RED (the smuggled breaking changeset under .changeset/pre/ is no longer reported)

Other gates, measured on the same cut commit rather than reasoned about:

  • pr-automation.yml's changeset counter (--diff-filter=A … '.changeset/*.md'): the 209 moves report as R100, so it credits a PR with 0 — still 0 with diff.renameLimit=1 forced; only --no-renames turns them into 209 A rows, and nothing passes it. No change.
  • The AMR scan both gates use returns exactly 209 R100 rows over the cut. Pure renames compare equal at the old path, so every one is exempt — but only because the filter says AMR. That is now the ordinary path, not a crafted bypass, and the header says so.
  • Root-only readers (readdirSync filtered to .md) see pre as a directory and ignore it: after the GA pass, readdirSync('.changeset') is README.md, config.json, pre and pending stock is 0. The post-exit mass deletion is 211 D, of which 210 are under .changeset/pre/ and 0 in the root.

6. What did NOT change — measured, not assumed

7. #9450 — decided, not folded in silently

Not carried.cut-rc.yml:285's "range is walkable" preflight is a different defect in the same file: it is about objectui clone reachability, and nothing in this migration touches it. It is an ungraded finding, #9497 explicitly did not exercise the objectui pin-range path, and grading it would be fresh adjudication rather than execution against a measured spec. The two blocks are textually disjoint, so nothing is gained by bundling. It stays open and unassigned for its own dispatch.

8. Changeset decision — skip-changeset

This PR releases nothing. The diff is a root devDependency range plus release machinery: two gate scripts, two workflows' comments, an internal maintainer doc, and .changeset/config.json. No workspace package's published surface, runtime, or types are touched; docs/ is not content/docs/. Naming a package in a changeset would put a false line in 69 CHANGELOGs. The label is the exemption designed for exactly this, so it is applied rather than an empty changeset written (#5471/#4898).

9. Gates — re-derived for this diff and run

node scripts/pm/dispatch-gates.mjs over the 8 changed paths named 13 families; all 13 run green on the final commit — git rev-parse --short HEAD at the time of the run: 06e7ca712:

check:changeset-gate-self-tests ✓ 212 + 116 + (check-empty-changeset) assertions
check:node-version ✓ 27 setup-node steps, all Node 22
check:objectui-changeset ✓ digest + range self-tests
check:override-consistency ✓ 4 published-manifest declarations resolve
check:required-contexts ✓
check:shard-attestation ✓ 92 assertions
check:workflow-status-functions ✓ 34 assertions, 24 workflows scanned
check:nul-bytes ✓ 75 assertions; 6157 files, no raw control bytes
check-adr-0087-registration.mjs --base MERGE_BASE ✓
check-changeset-fixed.mjs ✓
check-changeset-no-major.mjs --base MERGE_BASE ✓
check-empty-changeset.mjs --base MERGE_BASE ✓
check-osv-exemptions.mjs ✓
eslint on both changed scripts ✓

10. For whoever dispatches the next cut

cut-rc.yml asserts computed == dispatched. From today's stock the computed number is 17.1.0-rc.0, not 18.0.0-rc.0. Dispatch the new number, or the cut refuses before anything is pushed or published.

Rehearsal rig — reproducible, and why it terminates

Per #9555: a local pnpm run version never terminates in this container, because @changesets/git's getCommitsThatAddFiles deepens in a loop against a shallow clone with no reachable remote. The scaffold, inside a throwaway clone with its remote removed:

git rm -r --cached --quiet .changeset && git commit -q -m "replay scaffold: untrack .changeset"
git add .changeset && git commit -q -m "replay scaffold: re-add .changeset"

Tree hash verified identical before and after (7c25e1c0f04822a9fd29f5718b9c1ec32a509fea). A full pnpm run version then takes 7.7s. No release was performed: no publish, no tag, no pre-mode transition on origin, no workflow dispatch, no Version Packages PR touched. .changeset/pre.json is absent on main and stays absent on this branch.


Generated by Claude Code

…fig.json
- privatePackages: {version:true, tag:false} (U4)
- drop ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH (U2)
- $schema -> @changesets/config@4.0.0 (the version v3 resolves)
… and docs
- check-adr-0087-registration.mjs: exclude .changeset/pre/ from the audit
surfaces (--list / --audit-stock) while the enforcing diff scan keeps
judging it; PRE1-PRE3 fixtures
- check-changeset-no-major.mjs: header only — v3 numbers, and the stale
claim that both siblings pass --diff-filter=AM (they pass AMR since #7045)
- cut-rc.yml / pr-automation.yml / docs/releases-maintenance.md: the prose
asserting consumed changesets are recorded in .changeset/pre.json
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling labels Aug 18, 2026
@os-project-manageros-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026 — with Claude
@os-project-manager
os-project-manager marked this pull request as ready for review August 18, 2026 10:30
@os-project-manager
os-project-manager added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit e7daea1Aug 18, 2026
28 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-9498-changesets-v3-migration branch August 18, 2026 11:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3 migration 2/4] Land the v3 migration as one atomic PR — bump, gates, workflows and config together

2 participants

@os-project-manager@claude