Skip to content

feat(pm): refuse a doomed pnpm run version rehearsal with a diagnosis instead of hanging - #9620

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9555-version-rehearsal-shallow
Aug 18, 2026
Merged

feat(pm): refuse a doomed pnpm run version rehearsal with a diagnosis instead of hanging#9620
os-steve merged 1 commit into
mainfrom
claude/issue-9555-version-rehearsal-shallow

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9555

A local pnpm run version rehearsal in an agent container does not fail — it hangs, at full CPU, with a clean git status and no output, which reads as slow progress. This ships the refusal that turns that into a one-second diagnosis, the repair for a throwaway clone, and the recipe that points at both.

Measured before designing (the assumptions the dispatch asked me to test)

All on this container, 2026-08-18, against a real clone of /home/user/objectstack (126 consumable changesets):

probemeasuredverdict
getCommitsThatAddFiles over 5 real changeset paths, @changesets/git@4.0.0 (what pnpm-lock resolves)killed at 30 s wall / 22.6 s CPU, exit 124never terminates
git fetch --deepen=50 with no remote configured0.009 s, exit 0, nothing gainedsilent no-op
git fetch --deepen=50, origin = the local shallow checkout0.36 s, exit 0, still 50 commitssilent no-op
git fetch --unshallow from that same shallow source0.275 s, exit 0, still shallowsilent no-op
git fetch --unshallow from github.com in a throwaway clone10.2 s, +36 MB, 9952 commits, no longer shallowworks
the card's offline scaffoldtree hash identical (38904fdf12780fa442792eebb75778ef79080642), clean statusworks

So the first assumption resolves in the worst direction: the deepen the retry loop depends on succeeds and gains nothing, exiting 0 every time, so no error is ever raised and the loop is genuinely infinite rather than slow. "Make it fail fast" is not available — nothing here fails.

Both remedies were then verified with the real package on the real tree: unshallowed clone resolves all 126 changesets in 3.9 s to their real add-commits (faithful changelog links); the scaffolded clone resolves them in 3.6 s but to one fabricated commit (placeholder links). The guard offers both and says which is which.

Reproducible on demand: yes, every time, in three lines (clone the container checkout, point getCommitsThatAddFiles at its changesets, watch it not return).

What ships

  • scripts/pm/release-rehearsal-clone.mjs — run with no flags it only diagnoses (mutates nothing, ~1.1 s on a full checkout) and exits 2 with the mechanism plus the remedies; --prepare repairs a throwaway clone (offline scaffold with the tree hash asserted identical and rolled back if it is not, plus the base branch changeset status needs) and refuses to fabricate commits in anything with a network remote — in this container /home/user/objectstack is itself shallow, and two scaffold commits on a live branch is exactly the damage that must not be automated. Verified read-only against the shared checkout: it reports UNFIT and offers fetch --unshallow, never the scaffold.
  • The predicate is the parentless add-commit, not --is-shallow-repository. A shallow clone whose changesets were added after the boundary resolves on the first pass and is fine; refusing on shallowness alone would cry wolf on it. On the real container clone the split is visible: 107 of 126 parentless, 19 inside the 50-commit window.
  • The second symptom, which arrives first. A throwaway clone of a container checkout has no local main at all, so changeset status dies in getDivergedCommit; the obvious workaround --since origin/main then exits 0 reporting nothing, which reads as "no packages to release". The guard reports it as its own finding with its own remedy.
  • docs/releases-maintenance.md gains a "Rehearsing the version pass (throwaway clone)" section immediately before "Cutting a release" — the recipe with the preflight inside it, which is where the next scheduled rehearsal's runner looks.
  • lint.yml runs the --self-test (31 assertions on real git fixtures): the trapped shape refuses, a healthy tree and a merely-shallow-but-fine tree pass untouched, --prepare leaves the tree hash unchanged, both refusal paths commit nothing, and the wiring is pinned so unlinking the script from the doc or from the step reddens instead of going quiet.

What is deliberately NOT in this PR

The unmissable wiring — a preflight prefix on the version script — would edit root package.json, which is declared territory of the @changesets/cli v3 migration epic (#9465), so it is a hand-off, not something to route around via an adjacent file. The whole change is one prefix if that lane wants it: node scripts/pm/release-rehearsal-clone.mjs --check && changeset version && …, a no-op wherever history is full (every CI checkout of this repo, fetch-depth: 0). For the same reason the self-test is invoked as node … rather than a pnpm check:* alias — the alias line also lives in root package.json. Both are stated in the step comment and the script header.

No changeset: this ships a local tooling script, a docs section and a CI step — nothing published (skip-changeset).

Gates (run after the final commit, a512043d6)

Derived from the changed paths with node scripts/pm/dispatch-gates.mjs scripts/pm/release-rehearsal-clone.mjs docs/releases-maintenance.md .github/workflows/lint.yml:

  • pnpm check:nul-bytes — OK (6171 files, self-test 75 assertions)
  • pnpm check:node-version — OK (27 setup-node steps across 24 workflows)
  • pnpm check:required-contexts — OK
  • pnpm check:shard-attestation — OK (self-test 92 assertions)
  • pnpm check:workflow-status-functions — OK (24 workflows, 43 jobs)
  • pnpm check:type-check-coverage — OK
  • pnpm check:doc-anchors — OK (247 fragment links) · pnpm check:doc-authoring — OK (377 files)
  • pnpm check:pm-dispatch-gates — OK (273 cases) · pnpm check:pm-skill-id-lint — OK · pnpm check:ratchet-remedy-authority — OK
  • pnpm exec eslint scripts/pm/release-rehearsal-clone.mjs — clean
  • node scripts/pm/release-rehearsal-clone.mjs --self-test — 31 assertions, green
  • pnpm check:type-check-debtnot run locally: it refuses without a built workspace closure (turbo run build over all packages) and this diff contains no TypeScript, no package and no tsconfig. CI runs it with the closure built.

Generated by Claude Code

…is instead of hanging (#9555)
A local version rehearsal in an agent container never terminates: every clone
descends from a shallow checkout, so each `.changeset/*.md` resolves to the
parentless shallow boundary and @changesets/git deepens-and-retries in a loop
with no attempt limit — against a remote that gains it nothing while exiting 0.
Measured here: no remote 0.009s, local shallow origin 0.36s, `--unshallow` from
that source 0.275s, all exit 0, none gaining a commit. It reads as slow progress
and cost ~2.5h before diagnosis.
`scripts/pm/release-rehearsal-clone.mjs` diagnoses the clone (mutating nothing)
and refuses with the mechanism plus both measured remedies; `--prepare` repairs a
throwaway clone (offline scaffold, tree hash asserted identical, plus the base
branch `changeset status` needs) and refuses to fabricate commits in anything
with a network remote. Its `--self-test` pins both directions and the wiring, and
runs in lint.yml. docs/releases-maintenance.md now prescribes the rehearsal with
the preflight in it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation ci/cd labels Aug 18, 2026
@os-steve
os-steve marked this pull request as ready for review August 18, 2026 13:45
@os-steve
os-steve added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 5f99739Aug 18, 2026
24 checks passed
@os-steve
os-steve deleted the claude/issue-9555-version-rehearsal-shallow branch August 18, 2026 14:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude