Skip to content

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

Description

@os-project-manager

Sub-issue of #9465.

Blocked-by: #9497

Do not start before #9497's measurements are on #9465. The scope of this card is those measurements; starting early means writing the migration against a guessed shape, which is the failure this epic is structured to avoid.

Ruling — one PR, not a sequence

Both half-states are broken, and each is broken silently until the next release:

  • bump alone → v3 installed while the gates and cut-rc.yml still model v2's pre-mode semantics;
  • migration alone → gates expecting .changeset/pre/ while v2 never creates it.

So this PR carries @changesets/cli: ^3.0.0 in the root package.json and every consequential change. #9217 (Dependabot's raw bump) closes itself once main carries ^3.0.0.

Expected surface — re-derive it from #9497's verdicts, do not copy this list

package.json (the bump; and the version script is a && chain — changeset version && sync-protocol-version.mjs && sync-template-versions.mjs && sync-docs-image-tags.mjs — so v3's exit-1 behaviour reaches all of it) · .changeset/config.json ($schema, privatePackages per U4, the experimental key per U2) · scripts/check-changeset-no-major.mjs (the pre/RC exemption, per U5) · scripts/objectui-changeset-digest.mjs (reads .changeset/pre.json, and its self-test fixtures write {"mode":"pre","tag":"rc"}) · scripts/check-empty-changeset.mjs (only if U1 measured exact coverage; the default is that this gate stays) · .github/workflows/cut-rc.yml (the enforced allowlist and the prose describing v2 semantics) · .github/workflows/pr-automation.yml (prose referencing pre.json behaviour and #4898).

Non-negotiable

  • The allowlist follows the measurement, never the reverse. If a v3 changeset version produces a file set cut-rc.yml would reject, the allowlist is updated to the measured truth — you do not massage the tool's output to fit a list written under v2.
  • A self-test that still asserts v2 semantics is a failed run, not a passing one. Every touched script's --self-test must model v3 and pass.
  • Perform no release. No changeset publish, no version tags, no release-workflow dispatch, no merging of a Version Packages PR, and no pre-mode transition on origin. Releases are human-only here and this card does not change that.
  • Do not weaken a gate to accommodate v3 before [v3 migration 1/4] Rehearse @changesets/cli v3 in a throwaway clone and answer U1–U6 with measurements — no repo changes #9497 measured what v3 actually does.

Acceptance

  • One PR, first body line Fixes #<this card>, carrying the bump and every consequential change together.
  • PR body carries the rehearsal record from [v3 migration 1/4] Rehearse @changesets/cli v3 in a throwaway clone and answer U1–U6 with measurements — no repo changes #9497 (measured file set vs the allowlist) and states which of U1–U6 drove which change.
  • Every touched script's --self-test passes and models v3; gates re-derived for the actual diff with node scripts/pm/dispatch-gates.mjs and run.
  • Changeset decision stated and justified: a root devDependency change reaches no consumer, so skip-changeset may be correct here — decide from the diff and say why.
  • pnpm changeset, changeset status and a dry pnpm run version all exercised after the change, with output quoted.

Activity

  1. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Decision required before this card can be written — the migration changes the release number

    #9497's rehearsal measured it on both CLIs from identical stock, with no changeset declaring a major:

    computed next RC
    @changesets/cli 2.31.1 (today) 18.0.0-rc.0
    @changesets/cli 3.0.0 17.1.0-rc.0

    Traced by ablation, not inferred: deleting @objectstack/cli's peerDependencies block flips v2's plan from {"major":69,"patch":7,"none":1} to {"minor":69,...}. The major digit today comes from one optional peer edge on @objectstack/driver-turso interacting with "a prerelease never satisfies a non-prerelease range" — not from anything an author wrote.

    This is blocking rather than cosmetic: cut-rc.yml asserts the computed version equals the dispatched one, so the first cut after this card lands refuses a dispatch written from the old expectation.

    Not adjudicable by this seat. Release numbering is a maintainer call under the release guardrail, and it is exactly the class where the mechanical test says escalate: it changes a published version's meaning, and it is not reversible once a train ships under it.

    Options

    • A — accept 17.1.0. Treat v2's 18.0.0 as the artifact it is, and let the convention already enforced in check-changeset-no-major.mjs mean what it says: a major is declared deliberately or not at all.
    • B — preserve 18.0.0. Land a deliberate major-declaring changeset (or an explicit version input) in this same PR, so the number the last several trains were built on carries forward.
    • C — defer to [v3 migration 4/4] Verify the migrated release path inside the next real RC window #9500, settling numbering inside the next real RC window.

    Four prongs

    • Platform long-term coherence — A shrinks a special case: the version stops being a function of a dependency-graph detail no author can see. B keeps that coupling alive, which is the exact class of hidden mechanism this epic exists to remove.
    • Measured business pull — nothing downstream was found consuming the major digit as a promise. v2's 18.0.0 was never anyone's decision. "Preserving" it preserves an accident.
    • AI-agent error-resistance — under A, "what version does this cut produce" is derivable from changesets a human or agent can read. Under B the trap stays armed and undocumented, and the next person to hit it has no way to see why.
    • Startup scope discipline — A costs nothing to adopt; B costs a fabricated major-declaring changeset whose stated reason would be "because the old tool computed it that way".

    PM recommendation: A, with the ruling recorded here before #9498 is written. C is the weakest of the three — it defers a decision that changes what this card's own tests must assert, and #9500 cannot run until a real window opens.

    This card is needs-user-decision until that ruling lands. Its other blocker (Blocked-by: #9497) is now cleared — the rehearsal is done and the scope is on #9465.


    Generated by Claude Code

  2. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Ruling: option A — the next train is 17.1.0

    Maintainer's decision, quoted verbatim:

    接受 17.1.0

    Recorded as binding on this card and on #9500. What it settles, so nobody re-derives it later:

    • The next RC computes 17.1.0-rc.0 under v3, from the same stock that computes 18.0.0-rc.0 under v2. That difference is expected and accepted, not a defect to chase.
    • v2's major digit is understood as an artifact — one optional peerDependency from @objectstack/cli onto @objectstack/driver-turso, interacting with "a prerelease never satisfies a non-prerelease range". No changeset declared it and no downstream was found consuming it as a promise.
    • The repo's existing convention now means what it says: a major is declared, not inferred. No compensating major-declaring changeset is to be written to preserve 18.0.0 — that was explicitly rejected.

    Consequences this card must carry:

    1. The PR states the expected next computed version and shows it — a changeset status reading under v3 on the migrated tree, so the number is evidence rather than a claim.
    2. Whatever this card's tests assert about versioning must assert 17.1.0, not the old expectation.
    3. cut-rc.yml asserts computed == dispatched, so the first dispatch after this lands must be written from the new number. That is a note for whoever runs the next cut, and it belongs in the PR body where they will look.

    needs-user-decision is cleared; this card returns to the queue.

    Sequencing, decided now rather than discovered later: this card edits cut-rc.yml's staging/allowlist block, and #9518 is in flight on that exact block (a p0 defect that blocks the next cut on today's v2, independent of this migration). Same-file work serializes: #9518 lands first, this card rebases onto it. Being unblocked by the ruling does not make it dispatchable while a sibling holds the file.


    Generated by Claude Code

  3. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: epic #9465, round 3
    Session: session_01HDA9nN6nXQngoQUAAzRdMb
    Branch: claude/issue-9498-changesets-v3-migration
    Worktree: objectstack-issue-9498
    Domain: domain:devx
    File surface: package.json (the @changesets/cli range) · .changeset/config.json · scripts/check-changeset-no-major.mjs · scripts/objectui-changeset-digest.mjs · scripts/check-adr-0087-registration.mjs · .github/workflows/cut-rc.yml (prose only — see below) · .github/workflows/pr-automation.yml (prose only) · docs/releases-maintenance.md (stop on breach; explain in the report)
    Container & model: L, mode:subagent, model: fable — the epic's heaviest card: it rewrites the gates that decide whether a release commit may be pushed. If fable is measurably unavailable (quota), it falls to opus under the maintainer's 2026-08-13 exemption and the downgrade gets recorded here with its reason; the exemption covers dispatch only.
    Serial constraints cleared: #9518 / PR #9553 is MERGED (955ccf20d) — the cut-rc.yml block this card also touches is now settled on main, and this branch starts from a main that contains it. #9450 remains an ungraded finding with no branch and is deliberately not carried. No other open PR touches the epic's declared territory.

    Both blockers are cleared: #9497's rehearsal is done and its verdicts are on #9465, and the version-number question is ruled — 17.1.0.

    Five constraints this dispatch carries, each bought with a measurement rather than an opinion:

    1. 17.1.0, proven with a changeset status reading under v3 on the migrated tree — shown, not asserted.
    2. Branch from a main containing 955ccf20d; the SURFACE_LIST / grep -vxF block is not to be reverted or restructured. Only the stale pre.json prose in that file changes.
    3. The rehearsal must use [finding] Any local pnpm run version rehearsal hangs forever in a shallow clone — getCommitsThatAddFiles deepens in a loop that never terminates without a reachable remote #9555's shallow-clone scaffold. Without it pnpm run version never terminates in this container — that cost 2.5 hours once already and must not be paid twice.
    4. .changeset/config.json gains "privatePackages": {"version": true, "tag": false} — measured on [v3 migration 1/4] Rehearse @changesets/cli v3 in a throwaway clone and answer U1–U6 with measurements — no repo changes #9497 to reproduce v2's plan exactly, private-package versions included.
    5. scripts/check-empty-changeset.mjs is untouched. U1 measured that v3's exit-1 does not cover the all-empty-frontmatter shape, so the gate that catches 空 changeset 会静默卡死已 version 的发布:Release run 全绿,但 npm 和 Docker 什么都没发(17.0.0-rc.2 现在就卡着) #4898 stays.

    Generated by Claude Code

  4. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Dispatch 1 died on the model quota wall before producing anything: You've reached your Fable 5 limit. Verified clean, so this is a re-dispatch and not a hand-over — git ls-remote origin 'refs/heads/claude/issue-9498*' returns nothing and no worktree exists.

    Re-dispatched at model: opus under the maintainer's standing quota exemption (2026-08-13), quoted verbatim:

    fable 如果用完了,可以用 opus

    Amending this card's Container & model line to L, mode:subagent, model: opus. The exemption's floor is opus and nothing goes below it. This is the second fable wall today — #9228 hit the same one this morning — so the top tier should be treated as unavailable for the rest of this session rather than retried per card.

    What that costs, stated rather than glossed: this is the epic's heaviest card, and it is being written one tier below what it was graded for. The compensations are already in place and are the reason this is acceptable rather than merely necessary — the card carries five constraints that are measured, not judgment calls (#9497 did the thinking at the tier that was available then), and the review is mine, against origin/main, with the rehearsal's numbers to check the PR's claims against. A card whose hard decisions were pre-made and pre-measured is exactly the kind that survives a downgrade; one that still had open design questions would not, and would have waited instead.


    Generated by Claude Code

  5. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Draft PR #9560 is open. Report below; the PR body carries the full evidence.

    {
      "issue": 9498,
      "status": "done",
      "branch": "claude/issue-9498-changesets-v3-migration",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9560",
      "premise_still_valid": true,
      "summary": "@changesets/cli 2.31.1 -> 3.0.0 landed atomically with every consequential change: .changeset/config.json gains privatePackages {version:true,tag:false} (U4) and loses ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH (U2, measured inert under v3), $schema moves to @changesets/config@4.0.0 (the version v3 resolves); check-adr-0087-registration.mjs excludes .changeset/pre/ from its --list / --audit-stock surfaces while the enforcing diff scan keeps judging it (PRE1-PRE3 fixtures, both ablations run); check-changeset-no-major.mjs, cut-rc.yml, pr-automation.yml and docs/releases-maintenance.md get prose only, correcting the v2 claim that consumed changesets are recorded in .changeset/pre.json. 17.1.0 is proven, not asserted: changeset status under v3 on the migrated tree reads 17.1.0 (GA) and 17.1.0-rc.0 (pre), and a full window driven in a throwaway clone lands RC1 17.1.0-rc.0, RC2 17.1.0-rc.1, GA 17.1.0 with major releases: NONE. One divergence from #9497's rehearsal, and it cuts toward the fix: with privatePackages unset the rehearsal measured lossy-but-survivable, whereas on today's 209-changeset stock changeset status EXITS 1 (Found mixed changeset default-timeout-margin-repair: @objectstack/dogfood ignored + @objectstack/types not ignored). That changeset landed after the rehearsal's snapshot, so shipping the bump without U4's key would not merely mis-version private packages, it would stop the release lane from running at all. #9450 is explicitly NOT carried and the reasoning is in the PR body. check-empty-changeset.mjs is untouched and U1 was re-measured here rather than cited. The SURFACE_LIST / grep -vxF block from 955ccf20d is untouched: only prose and one echo string change in that file. 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 stays absent on the branch.",
      "tests": "All gates re-derived with `node scripts/pm/dispatch-gates.mjs` over the 8 changed paths (13 families named) and run on the final commit 06e7ca712, which is PR #9560's head. GREEN: check:changeset-gate-self-tests (check-adr-0087-registration 212 assertions, check-changeset-no-major 116, check-empty-changeset), check:node-version, check:objectui-changeset, check:override-consistency, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check:nul-bytes (6157 files, no raw control bytes), check-adr-0087-registration.mjs --base MERGE_BASE, check-changeset-fixed.mjs ('fixed group is in sync with 69 public workspace packages'), check-changeset-no-major.mjs --base MERGE_BASE, check-empty-changeset.mjs --base MERGE_BASE, check-osv-exemptions.mjs, and eslint on both changed scripts. REHEARSAL (throwaway clone, remote removed, #9555's scaffold; tree hash 7c25e1c0f04822a9fd29f5718b9c1ec32a509fea identical before/after the scaffold; full pnpm run version in 7.7s): changeset status -> {\"minor\":69,\"patch\":7,\"none\":1}, 209 changesets, spec 17.0.0 -> 17.1.0, major releases: NONE; in pre mode spec 17.0.0 -> 17.1.0-rc.0. Pre-mode version pass produced 366 porcelain lines = 209 D .changeset/NAME.md + 76 M package.json + 76 M CHANGELOG.md + 3 M declared doc surfaces + 2 untracked (.changeset/pre.json, .changeset/pre/), and cut-rc.yml's staging plus allowlist replayed verbatim accepted all 365 staged paths with the unstaged assertion clean. REVERSE VERIFICATION, direction predicted before running: ablating the pre/ filter turned PRE1 and PRE3 RED (PRE1 got the two .changeset/pre/consumed-*.md paths in stock; PRE3 got pending stock 1 where 0 was expected) while PRE2 stayed GREEN by design; moving the filter into isChangesetFile (the verdict path) turned PRE2 RED (the smuggled breaking changeset under .changeset/pre/ stopped being reported). U1 RE-MEASURED on v3.0.0 rather than cited: three empty-frontmatter changesets -> `changeset version` exits 0, deletes all three, bumps nothing (that is #4898, reproduced on v3); zero changesets -> exits 1. Gate exposure measured on a real v3 cut commit: `git ls-tree -r ... -- .changeset` lists 209 .changeset/pre/* entries; the AMR scan returns exactly 209 R100 rows; pr-automation.yml's --diff-filter=A counter credits 0 (still 0 with diff.renameLimit=1; 209 only under --no-renames). Post-exit GA pass: 211 deletions, 210 under .changeset/pre/ and 0 in the root, so the three gates' root-only readdirSync readers see nothing.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9561: two comments (pr-automation.yml:783 and check-changeset-no-major.mjs:195) still describe the launch-window major guard as dormant because the repo is in pre mode; .changeset/pre.json has been absent since the 17.0.0 GA exit, so the guard is armed. Not folded in: nothing about v3 causes it, and correcting the #6923 coverage note reverses a present-tense claim about what CI has ever executed - a judgement this card was not scoped to make.",
        "not filed, recorded on #9555 as a comment: `changeset status` with no --since dies in getDivergedCommit before it ever reaches the hang #9555 documents, and `--since origin/main` silently returns an EMPTY plan because @changesets/read uses sinceRef to restrict which changesets it reads. Same root cause as #9555 (shallow clone, no reachable remote), second entry point; it belongs on that card rather than a new one, and #9500 will hit it first."
      ]
    }

    Three things worth the PM's eye before ACCEPT

    1. The divergence is the interesting result. [v3 migration 1/4] Rehearse @changesets/cli v3 in a throwaway clone and answer U1–U6 with measurements — no repo changes #9497 measured privatePackages unset as survivable. On today's stock it is fatal (Found mixed changeset default-timeout-margin-repair -> exit 1), because one changeset that landed after the rehearsal's snapshot names @objectstack/dogfood (private) alongside @objectstack/types (public), and v3 treats unversioned private packages as ignored. The rehearsal did not mispredict; the stock moved under it. Ruling 1 gains a second, independent reason.
    2. cut-rc.yml's pin-range preflight asserts "the digest will be complete" from an object-presence test that cannot support it #9450 is decided, not deferred silently: not carried, reasoning in PR body section 7. It stays open and unassigned.
    3. The next cut must be dispatched as 17.1.0-rc.0. cut-rc.yml asserts computed == dispatched, so a dispatch written from the old 18.0.0 expectation refuses before anything is pushed. This is in the PR body where whoever runs the cut will look.

    Generated by Claude Code


    Generated by Claude Code

  6. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    ACCEPT. Each constraint checked against the branch and origin/main directly, not against the report.

    constraint how I verified it result
    (a) 17.1.0 proven PR body carries changeset status output under v3 on the migrated tree, plus a full pre enter → RC1 → RC2 → pre exit → GA window with per-step counts shown, not asserted
    (b) 955ccf20d not reverted diffed cut-rc.yml non-comment lines against the merge base: exactly one changed line, an echo string SURFACE_LIST / grep -vxF block intact
    (c) #9555's scaffold used full pnpm run version completed in 7.7s where it previously never terminated scaffold worked
    (d) privatePackages read the config.json diff {"version": true, "tag": false} present
    (e) check-empty-changeset.mjs git diff --name-only for that path untouched

    Also verified independently: package.json carries ^3.0.0; the unsafe ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH key is gone and $schema moved to @changesets/config@4.0.0 (epic benefit 3, realized); the 8 changed paths all sit inside the declared territory, with pnpm-lock.yaml inherent to the bump rather than scope creep; and objectui-changeset-digest.mjs's only pre.json contact really is pre?.mode === 'pre', which survives v3's slimmed file — so leaving it untouched is correct rather than an omission.

    The divergence is the most valuable thing in this run. #9497 measured privatePackages unset as lossy-but-survivable. On today's 209-changeset stock it is fatal: changeset status exits 1 on a mixed changeset (@objectstack/types public + @objectstack/dogfood private in one file), which landed after the rehearsal's snapshot. So constraint (d) was not a versioning nicety — without it the bump takes the release lane from "wrong numbers" to "will not run at all". The card was told the measurement wins over the rehearsal, and it did, in the direction that mattered.

    Two pieces of honest reporting worth recording, because both cut against the PR's own case:

    1. U5's payoff is less than the epic hoped. The pre/RC exemption switch, the mode: "exit" re-arm and the merge-base/AMR scoping all stay load-bearing under v3. What v3 retires is the urgency the header was written in, not the code. The header now says so, which is better than silently shrinking a guard.
    2. U1 was re-measured on v3 rather than cited — three empty-frontmatter changesets still exit 0 and bump nothing. That is 空 changeset 会静默卡死已 version 的发布:Release run 全绿,但 npm 和 Docker 什么都没发(17.0.0-rc.2 现在就卡着) #4898 reproduced on v3, and it is why check-empty-changeset.mjs stays. A card that could have quietly leaned on the rehearsal's word re-ran it instead.

    #9450 was answered rather than left hanging: not carried, with the reasoning (a textually disjoint objectui-reachability defect that grading would require fresh adjudication) in the PR body. That was a hard requirement of the dispatch and the failure mode it guarded against — silent folding — did not happen.

    skip-changeset is right and justified from the diff: a root devDependency plus release machinery, no published surface touched. Naming a package would put a false line in 69 CHANGELOGs.

    CI was still converging at review time (Test Core, Temporal Conformance, Dogfood, Build in progress); the 13 gate families were re-derived and run green locally at 06e7ca712. Flipping to ready and enabling auto-merge — it cannot merge while anything is red.


    Generated by Claude Code

  7. os-project-manager commented on Aug 18, 2026

    @os-project-manager
    CollaboratorAuthor

    Landed and verified on origin/main as e7daea169 — chore(release): migrate the release toolchain to @changesets/cli v3 (#9560).

    Checked by content, not by sha ancestry (the queue lands as a squash, so the reviewed sha is not an ancestor of main):

    origin/main:package.json            "@changesets/cli": "^3.0.0"
    origin/main:.changeset/config.json  "privatePackages": { "version": true, "tag": false }
    origin/main:.changeset/config.json  ___experimentalUnsafeOptions…  → 0 occurrences (removed)
    origin/main:.github/workflows/cut-rc.yml  SURFACE_LIST → 6 occurrences (955ccf20d intact underneath)
    

    That last line is the one worth having checked. This card edited the same workflow the p0 fix had just rewritten, and the constraint was that its SURFACE_LIST / grep -vxF block survive untouched. It did — on main, after both landed, not merely on the branch.

    What is now true that was not this morning: the release toolchain runs v3; private packages keep versioning without tagging; the unsafe experimental key that upstream reserved the right to change in a patch release is gone; and the next RC cut computes 17.1.0-rc.0, which is the number whoever dispatches it must write, because cut-rc.yml asserts computed == dispatched.

    Closing as completed. The epic's remaining cards are #9499 (awaiting a scope answer) and #9500 (on hold until the next real pre-mode window, which is where this migration gets its last and only end-to-end proof).


    Generated by Claude Code

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

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions