Skip to content

Commit 5f99739

Browse files
os-steveclaude
andauthored
feat(pm): refuse a doomed pnpm run version rehearsal with a diagnosis instead of hanging (#9555) (#9620)
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. Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3b36a19 commit 5f99739

3 files changed

Lines changed: 728 additions & 0 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,37 @@ jobs:
293293
- name: Governed-merges audit self-test
294294
run: pnpm check:pm-governed-merges
295295

296+
# Release-rehearsal clone preflight self-test (#9555). A local
297+
# `pnpm run version` rehearsal — the prescribed verification route for
298+
# every release-machinery change — HANGS FOREVER in an agent container:
299+
# every clone descends from a shallow checkout, so each `.changeset/*.md`
300+
# resolves to the parentless shallow boundary, and @changesets/git answers
301+
# a parentless add-commit by deepening and retrying in a loop with no
302+
# attempt limit — against a remote that gains it nothing and exits 0 while
303+
# doing so. Measured cost before diagnosis: ~2.5 h over two attempts,
304+
# because full CPU with no output reads as slow progress, not as a hang.
305+
# `scripts/pm/release-rehearsal-clone.mjs` is the refusal that turns that
306+
# into a one-second diagnosis, plus the repair for a throwaway clone.
307+
#
308+
# What runs HERE is the self-test, not the live check: the live check
309+
# judges whatever clone it is pointed at, and a CI checkout's depth is the
310+
# workflow's business rather than a verdict this job can hold. The
311+
# self-test pins BOTH directions on real git fixtures — the trapped shape
312+
# refuses, while a healthy tree AND a merely-shallow one whose changesets
313+
# sit after the boundary pass untouched (the predicate is the parentless
314+
# add-commit, never `--is-shallow-repository` alone) — and it pins the
315+
# WIRING, so unlinking the script from docs/releases-maintenance.md or
316+
# from this step reddens instead of going quiet. Temp-dir fixtures, no
317+
# network, ~1.7 s.
318+
#
319+
# Invoked as `node` rather than through a `pnpm check:*` alias: that alias
320+
# belongs in root package.json, which is declared territory of the
321+
# @changesets/cli v3 migration lane (#9465) while it runs. Same shape as
322+
# check-links.yml's ADR-link step, and dispatch-gates.mjs derives gate
323+
# families from either spelling.
324+
- name: Release-rehearsal clone preflight self-test
325+
run: node scripts/pm/release-rehearsal-clone.mjs --self-test
326+
296327
# Docs/skills authoring guard (#2035 / ADR-0059): TS code blocks in
297328
# Markdown/MDX are not type-checked or ESLinted, so skills/ and
298329
# content/docs/ can drift back to teaching the bare `: Page = {}` literal

‎docs/releases-maintenance.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,47 @@ node scripts/check-objectui-pin-fresh.mjs --ref v17.0.0 --json
214214
the bump is that ratchet's only trigger), then re-source the Console section with
215215
`scripts/objectui-range.mjs`.
216216

217+
## Rehearsing the version pass (throwaway clone)
218+
219+
Release-machinery changes are verified by replaying a real version pass in a
220+
**throwaway clone** — never in a working checkout, because the pass consumes
221+
`.changeset/*.md`, rewrites every `packages/*/CHANGELOG.md` and moves the
222+
`package.json` versions. Run the preflight first; it is the difference between a
223+
one-second diagnosis and a hang that looks like progress:
224+
225+
```bash
226+
git clone /home/user/objectstack /tmp/rehearsal && cd /tmp/rehearsal
227+
node scripts/pm/release-rehearsal-clone.mjs --prepare . # refuse, or repair
228+
pnpm install --frozen-lockfile
229+
pnpm run version # the rehearsal itself
230+
```
231+
232+
### Why the preflight exists (#9555)
233+
234+
A clone taken inside an agent container is **shallow** (it descends from a
235+
shallow checkout, and `.git/shallow` is inherited), and that breaks changesets in
236+
two places at once — same root cause, two entry points, and neither of them says
237+
so:
238+
239+
| symptom | dies in | what you actually see |
240+
|---|---|---|
241+
| `changeset version` never finishes | `getCommitsThatAddFiles`: every changeset resolves to the parentless shallow boundary, so it deepens and retries in a loop with no attempt limit | full CPU, no output, clean `git status` — reads as slow progress; the first attempt was allowed to run 70 minutes |
242+
| `changeset status` cannot answer | `getDivergedCommit` (`git merge-base main HEAD`) — a clone of a container checkout has no local `main` at all | `Failed to find where HEAD diverged from "main"`, and the obvious workaround `--since origin/main` then exits 0 reporting **nothing**, which reads as "no packages to release" |
243+
244+
Diagnosing that cost roughly two and a half hours the first time. The script
245+
carries the whole mechanism, the measurements and the two remedies in its
246+
header — `git fetch --unshallow` (10.2 s, +36 MB here, and it gives changelog
247+
links the *real* add-commits) or an offline two-commit scaffold that leaves the
248+
tree hash byte-identical. Run with no flags it only diagnoses and mutates
249+
nothing; `--prepare` repairs a throwaway clone and refuses to touch anything that
250+
looks like a real checkout. Its `--self-test` runs in `lint.yml` and pins both
251+
directions, so the refusal cannot rot into a guard that fires on everything or on
252+
nothing.
253+
254+
**This is a local-rehearsal trap only.** `cut-rc.yml` and `release.yml` check out
255+
with `fetch-depth: 0`, so a real cut has full history and never enters that
256+
branch.
257+
217258
## Cutting a release
218259

219260
Two routes, and the choice is not a preference — an rc and a GA release need

0 commit comments

Comments
 (0)