Skip to content

Migrate to @changesets/cli v3 — one PR carrying the bump plus the config changes v3's defaults require #5296

Description

@os-project-manager

Dependabot #4950 proposes @changesets/cli 2.31.1 → 3.0.0. Do not merge it as it stands: a major of this package is a release-tooling migration, and the bump alone leaves config that v3 reads differently.

The sibling repo finished this migration today (objectstack-ai/objectstack epic #9465, landed as e7daea169). Its measurements are reused here rather than re-derived, and the scope below already reflects them — most of what made that migration large does not exist in this repo.

What does NOT apply here — measured on origin/main, not assumed

objectui never enters pre mode: no .changeset/pre.json, and scripts/check-changeset-no-major.mjs contains zero references to it. There is no cut-rc.yml-style enforced file allowlist. So v3's largest breaking change — consumed prerelease changesets moving into .changeset/pre/ — has nothing to hit here. That was roughly three of the sibling epic's five cards; skip all of it.

What DOES apply, and none of it is visible to PR CI

  1. privatePackages default flips. v3 stops versioning private packages unless the key is set, and — this is the part that bites — it then treats them as ignored, so a changeset naming one ignored and one non-ignored package becomes a "mixed changeset" that changeset status refuses outright, exit 1. In the sibling repo that turned a "wrong versions" bump into a "release lane will not run" bump.

    Measured here: .changeset/config.json's ignore covers @object-ui/example-*, @object-ui/site, @object-ui/test-support. The private packages not covered are object-ui-monorepo (the workspace root) and object-ui at packages/vscode-extension/package.json — that one would be newly ignored under v3. Across the current 250 pending changesets, none names it, so the trap does not fire today. It fired in the sibling repo from a changeset that landed after the rehearsal snapshot, so "not today" is not a plan: set the key explicitly.

  2. ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange — same key as the sibling repo, which measured it inert under v3 (the peer-escalation branch is gone from @changesets/assemble-release-plan@7) but load-bearing under v2. It may therefore be removed only in the same commit that lands the bump, never before.

  3. $schema is pinned at @changesets/config@3.1.2; v3 resolves @changesets/config@4.0.0.

  4. changeset version now exits 1 when nothing is pending (it used to exit 0). .github/workflows/changeset-release.yml drives version: pnpm changeset:version, i.e. plain changeset version, through changesets/action@v1. Establish what that combination does when the action runs version with an empty plan — measure it, do not reason about it.

Why CI cannot tell you any of this: nothing in this repo's PR CI runs changeset status or changeset version. The only invocation is inside changeset-release.yml. A green #4950 proves the workspace still installs; it says nothing about whether a release still works.

Scope

One PR carrying all of it together — the bump and the config changes are broken apart, and each half is broken silently until the next release:

  • package.json: @changesets/cli → ^3.0.0 (plus the lockfile)
  • .changeset/config.json: add privatePackages, remove the experimental key, move $schema to @changesets/config@4.0.0
  • scripts/check-changeset-fixed.mjs and scripts/check-changeset-no-major.mjs: run their self-tests under v3 and fix whatever models v2 behaviour. A self-test that still asserts v2 semantics is a failed run, not a passing one.
  • .github/workflows/changeset-release.yml: only if item 4 measures a real change.

Dependabot's #4950 closes itself once main carries ^3.0.0.

Method

Rehearse in a throwaway clone with its remote removed — never against origin, and never in a pushable worktree. Install @changesets/cli@3.0.0 there and drive changeset status and changeset version against the real 250-changeset stock.

If a rehearsal hangs: the sibling repo hit @changesets/git's deepen loop spinning forever on a shallow clone with no reachable remote, and recorded the working scaffold at objectstack-ai/objectstack#9555 (untrack .changeset, re-add it in two commits — tree identical, resolution goes from never-terminating to sub-second). Check whether this checkout is shallow before assuming the tool is stuck.

Non-goals

  • Perform no release: no changeset publish, no version tags, no release-workflow dispatch, no merging of a version PR.
  • Do not enter pre mode. This repo has never used it and this card does not introduce it.
  • Do not touch changesets/action@v1's pin. The sibling repo declined that upgrade for a documented reason (objectstack-ai/objectstack#9208); whether the same reasoning applies here is a separate question, not a rider.

Acceptance

  • One PR carrying the bump and every config change together.
  • Evidence in the PR body, from the rehearsal on the real stock: changeset status output under v3 (no mixed-changeset error, and the computed plan), and a changeset version dry run.
  • Both changeset gate scripts' self-tests pass and model v3.
  • A changeset for this PR — and note the repo convention: objectui has no skip-changeset label. An empty-frontmatter changeset is the declaration mechanism here; do not mint a label.

Activity

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

    @os-project-manager
    CollaboratorAuthor

    Claimed and dispatched by the PM loop.

    • Session: session_01HDA9nN6nXQngoQUAAzRdMb
    • Branch: claude/issue-5296-changesets-v3
    • Worktree: ../objectui-issue-5296 (dedicated, per repo — no edits in the shared checkout, no git stash)
    • Model: opus. The contract-review tier (claude-fable-5) was measured unavailable twice today on quota; the 2026-08-13 exemption ("fable 用完了可以用 opus") applies, and this card is a config migration with a rehearsal, not a contract review.

    Routing authority, verbatim from the maintainer: "按照你的建议派发执行" — i.e. file the card and dispatch it, which is what this comment records.

    File territory this claim covers

    package.json, pnpm-lock.yaml, .changeset/config.json, scripts/check-changeset-fixed.mjs, scripts/check-changeset-no-major.mjs, .github/workflows/changeset-release.yml (last one only if item 4 in the body measures a real change), plus one .changeset/*.md.

    Nothing else. In particular this claim does not cover .github/workflows/dependabot-auto-merge.yml, the changesets/action@v1 pin, or any of the 250 pending changeset files.

    Serial constraints handed to the dev seat

    1. One PR. The bump and the config changes land together or not at all — split, each half is silently wrong until the next release.
    2. Rehearse in a throwaway clone with its remote removed. Never origin, never a pushable worktree.
    3. Perform no release. No changeset publish, no version tag, no release-workflow dispatch, no merging of a Version Packages PR.
    4. Measure, do not reason, for the changeset version-on-empty-plan question — the answer goes in the PR body either way.
    5. The gate scripts' self-tests must model v3. A self-test that still asserts v2 semantics and passes is a failed run.

    Dependabot #4950 stays open and unmerged until main carries ^3.0.0, at which point it closes itself.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    Correction to this card's body — a measurement I published here is wrong.

    The body states, under item 1: "Across the current 250 pending changesets, none names it, so the trap does not fire today." That is false. Verified independently against origin/main (7260a1ed4), not against the dev seat's report: .changeset/no-console-lint-rule.md names the private object-ui beside four published packages —

    '@object-ui/app-shell': patch
    '@object-ui/fields': patch
    '@object-ui/plugin-detail': patch
    '@object-ui/runner': patch
    'object-ui': patch
    

    So under v3 with privatePackages left undeclared, that is a mixed changeset and changeset status exits 1 on today's stock, not on some future one.

    Why I got it wrong: the private package's name is object-ui, which is a substring of all 40 @object-ui/* scoped names in those same files. Any unanchored search over .changeset/ either matches every file or, once filtered, hides the one line that matters. The check had to be anchored — ^'object-ui': — and I did not anchor it. One grep away, and I reported the loose result as a measurement.

    The correction strengthens the card rather than weakening it. The body's argument was "the trap does not fire today, but 'not today' is not a plan, so set the key explicitly." The real position is worse: dependabot's #4950, merged as it stands, would have stopped the release lane on the very next push to main — with PR CI green, because no PR workflow in this repo runs changeset status. The reason for the card holds; the margin I claimed did not exist.

    Found by the dev seat during rehearsal, which is where it should have been found — and it is on record here because a card body that stays wrong is worse than one that is visibly corrected.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor
    {
      "issue": 5296,
      "status": "done",
      "branch": "claude/issue-5296-changesets-v3",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5307",
      "premise_still_valid": true,
      "summary": "One draft PR (#5307, head c488fd7) carries the bump to @changesets/cli ^3.0.0 (resolves 3.0.1) with the lockfile, plus all three .changeset/config.json changes: explicit privatePackages {version:true, tag:false}, removal of ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange, and $schema moved to @changesets/config@4.0.0. Rehearsed in a throwaway clone with no remote, built by git archive from the branch HEAD so its tree hash equals the shipped commit's; changeset status exits 0 and the v3 release plan is identical to the v2 plan release-for-release (45 releases, 40 bumped 17.5.0 to 17.6.0, 249 changesets), and the changeset version dry run wrote 40 package.json bumps plus 40 CHANGELOGs, leaving all ignored packages untouched. ONE MEASUREMENT IN THE ISSUE BODY IS WRONG, in the direction that makes the card more urgent: it says no pending changeset names the private object-ui, so the trap does not fire today. It does fire today - .changeset/no-console-lint-rule.md names object-ui beside four published packages, so the bump alone exits 1 with 'Found mixed changeset no-console-lint-rule' on both changeset status and changeset version, with PR CI green. The card's other count is also off by the README: 250 .md files, 249 actual changesets. .github/workflows/changeset-release.yml is deliberately unchanged - see item 4 below.",
      "tests": "All at c488fd7 (working tree clean, = origin/claude/issue-5296-changesets-v3), run after the final commit. (1) vitest --project unit over check-changeset-fixed.test.ts, check-changeset-no-major.test.ts, check-changeset-presence.test.ts, merge-queue-reporting.test.ts: 'Test Files 4 passed (4) / Tests 66 passed (66)'. (2) pnpm changeset:check: green, now also printing 'privatePackages declared: version=true, tag=false'. (3) node scripts/check-changeset-presence.mjs: green ('No source of a released package changed in this range, so no changeset is owed', 1 changeset added anyway per repo convention). (4) pnpm check:control-bytes: green, 4688 tracked text files; plus a manual grep -naP over every file I touched: clean. (5) pnpm type-check:scripts: rc 0. (6) pnpm lint:coverage: 46/46 packages, 0 errors. (7) eslint on scripts/check-changeset-fixed.mjs and scripts/__tests__/check-changeset-fixed.test.ts: rc 0. REVERSE VERIFICATION, from the committed state, restored byte-identical to HEAD afterwards: deleting privatePackages turns the gate red (rule 1) and 2 of the new test's 13 cases red; setting version:false turns the gate red (rule 2) - and in that same state the CLI itself fails with the mixed-changeset error, so the gate's verdict coincides with the real one. Rehearsal ran on a tree whose hash (aac0c984a4e75c8c81670aec238a56c0b42f7b6b) equals the shipped commit's tree; the primary checkout IS shallow, so the objectstack#9555 deepen-loop trap is live here, but the two-commit scaffold avoided it (changeset version finished in 9s).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5306: root engines are looser than the toolchain now requires - node '>=22' admits 22.0-22.10 / 23.x / 25.x and pnpm '>=9' admits pnpm 9, all rejected by @changesets/cli@3's '^22.11 || ^24 || >=26' and 'pnpm>=10.0.0'. Nothing enforces it today (no engine-strict, packageManager pinned to pnpm@10.31.0, workflows on node-version 22.x), so it is a false declaration rather than a live break; tightening it affects every consumer and is a decision, not a rider on this bump. Filed unassigned, no labels."
      ]
    }

    Measurements per the four items

    1. privatePackages — default flips from {version:true, tag:false} (v2) to {version:false, tag:false} (v3), read out of @changesets/config@4.0.0; version:false feeds allowPrivatePackages:false into the assembler, i.e. private packages become ignored. Three settings rehearsed on the real stock: absent → exit 1 mixed changeset; {version:false,tag:false} → exit 1 mixed changeset; {version:true,tag:false} → exit 0 with a plan identical to v2. tag:false verified as the right half too — getPublishPlan.mjs filters private packages out of the npm publish set unconditionally, so only tagging is at stake. Moving object-ui to ignore was rejected: check-changeset-presence.mjs derives its guarded surface from fixed, so that would silently stop demanding changesets for the extension's source.
    2. Experimental key — @changesets/assemble-release-plan@6.0.10 references onlyUpdatePeerDependentsWhenOutOfRange 6 times in its dist, @7.0.0 references it 0 times: load-bearing under v2, inert under v3. @changesets/config@4.0.0 still parses it, so it would have been accepted-and-dead. A v3 run with the key re-added produced a byte-identical plan.
    3. Engines — ^22.11 || ^24 || >=26, "type": "module". Repo pin does not satisfy it; nothing enforces it today. Reported, not changed → 根 engines 声明比工具链实际要求宽:node ">=22" 放行 22.0–22.10 / 23.x / 25.x,pnpm ">=9" 放行 pnpm 9 —— @changesets/cli@3 全部拒绝 #5306.
    4. changeset version on an empty plan — the exit-code change is real (v2: exit 0 warn No unreleased changesets found, exiting.; v3: exit 1 No unreleased changesets found.), but a stock of only empty-frontmatter changesets exits 0 under both. And it is unreachable through this workflow: changesets/action@v1 resolves to v1.9.0, whose src/index.ts and compiled dist/index.js return via runPublish when there are no changesets and via "All changesets are empty; not creating PR" when all are empty — runVersion is reached only when at least one changeset carries a release. The action reads changesets with its own bundled @changesets/*, so this bump cannot change which branch it takes. No workflow change.

    Non-goals honoured

    No release performed (no changeset publish, no tag, no workflow dispatch, no version PR merged); pre mode not entered; changesets/action@v1 pin read but untouched; dependabot-auto-merge.yml and all 249 pre-existing changeset files untouched; #4950 left open and unmerged; no label minted.

    Beyond the claimed file territory (one file)

    scripts/__tests__/check-changeset-fixed.test.ts is new. The card asks for both gate scripts' self-tests to pass and model v3 — check-changeset-fixed.mjs had no self-test at all (only check-changeset-no-major and check-changeset-presence have one), so that criterion could not otherwise be met. check-changeset-no-major.mjs needed no change: it hand-parses frontmatter and must not import @changesets/* (it runs on a bare checkout), so nothing in it models v2.


    Generated by Claude Code


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    Landed. PR #5307 went through the merge queue at 08:43 and is on main.

    Verified by content on origin/main (e7c3b3e91), not by sha ancestry — the queue lands as a squash, so ancestry would prove nothing:

    • package.json → "@changesets/cli": "^3.0.0"
    • .changeset/config.json → $schema at @changesets/config@4.0.0, privatePackages: { "version": true, "tag": false } present, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH gone

    All four items in this card's body are answered on the PR, one of them by correcting the card: the mixed-changeset trap was live on today's stock, not hypothetical. Item 4 came back "no real change" — changeset-release.yml is untouched, with the pinned action's branch logic as the reason.

    Two things this does not yet establish, and I am not claiming them:

    1. Dependabot chore(deps-dev): bump @changesets/cli from 2.31.1 to 3.0.0 #4950 has not closed itself yet. It was still open on the read after the merge. It should retire on dependabot's next run now that main carries ^3.0.0; if it doesn't, that gets chased rather than merged by hand.
    2. The release lane has not run under v3 yet. changeset-release.yml is the only place changeset version is ever invoked, so the push to main is the first real exercise of this migration. Being watched; a failure there is a direct consequence of this change, not an unrelated event.

    Split out rather than ridden in: #5306 (root engines looser than the toolchain now requires).


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    Close-out — both items I left open above are now settled, and the migration is proven on the release lane rather than only in rehearsal.

    1. Dependabot #4950 retired itself. Closed 2026-08-19T08:46:02Z, three minutes after #5307 landed, and its branch is gone. Checked two ways (PR state, and git ls-remote for dependabot/npm_and_yarn/changesets/cli-3.0.0). My note above said it had not closed yet — that was true when written; no intervention was needed.

    2. changeset-release.yml ran green under v3. Run 32234037039 on e7c3b3e91, all steps success, including step 11 Create Release Pull Request or Publish to npm.

    This is the part rehearsal could not prove, so it is worth stating what the live lane actually did. The action took the hasChangesets → runVersion branch — it updated the existing version PR #4655 (touched at 09:12:57, the second step 11 finished) and did not publish, which is exactly the branch behaviour the PR body argued from changesets/action@v1.9.0's source. Read off origin/changeset-release/main (8715f50):

    object-ui (private, packages/vscode-extension) 17.6.0, still "private": true
    @object-ui/types (published) 17.6.0
    .changeset/ contents only README.md + config.json — all 249 consumed

    The private extension bumping in lockstep is the whole point of privatePackages: { "version": true, "tag": false }. Had the key been left undeclared — i.e. had #4950 been merged as it stood — this run would have failed with Found mixed changeset no-console-lint-rule instead, and it would have been the first anyone heard of it. The rehearsal's prediction (40 packages 17.5.0 → 17.6.0, 249 changesets consumed, README.md kept) matches the live output exactly.

    No release was performed and none will be from this lane by an agent. Version PR #4655 is open and now regenerated under v3; merging it is the release, and that is the maintainer's call, not this loop's. It is reported here, not touched.

    Card complete. Split out and still open: #5306 (root engines looser than the toolchain now requires).


    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

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions