Found while rehearsing #9518's fix. Cost roughly two and a half hours of wall clock across two attempts before it was diagnosed. Recording so the next rehearsal — #9500 is the scheduled one — does not pay it again.
What happens
changeset version resolves a commit for every consumed changeset to build changelog
links. @changesets/git's getCommitsThatAddFiles does this per path:
git log --diff-filter=A --max-count=1 --pretty=format:%H:%p .changeset/NAME.md
and then branches on whether the returned commit has a parent. A parentless commit is
treated as possibly being the boundary of a shallow clone rather than the real add, so:
- if
git rev-parse --is-shallow-repository is true, it calls deepenCloneBy({ by: 50 })
(git fetch --deepen=50) and retries those paths; - only if the repo is NOT shallow does it accept them and break.
Why it never terminates here
The agent container's /home/user/objectstack is itself a shallow clone. Any clone
taken from it inherits .git/shallow, and every .changeset/*.md resolves to the shallow
boundary commit, which by definition has no parent. The rehearsal recipe also says to use a
throwaway clone with no push remote — so git fetch --deepen=50 has no remote to fetch
from, remaining never shrinks, and the do ... while (remaining.size) loop spins forever.
Observable symptom: changeset version never finishes, git status stays clean, and the
same changeset filenames scroll past repeatedly in ps. It looks like slow progress, not a
hang, which is what makes it expensive — the first attempt was allowed to run 70 minutes on
the assumption that O(packages x changesets) was simply large.
Two dead ends worth naming: deleting .git/shallow makes git fail outright
(cannot simplify commit ... because of ... on the missing parent), and re-adding a remote
to the shallow source cannot help either, because deepening from a shallow source still
lands on a parentless boundary.
What does work
Give every changeset an add-commit that has a parent, inside the throwaway clone, before
running the version pass. The net tree is unchanged (verified: identical tree hash):
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"
A full resolution pass then takes 0.57s instead of never completing, and
pnpm run version runs end to end.
Not a production defect
cut-rc.yml checks out with actions/checkout@v7 and fetch-depth: 0, so the real cut has
full history and never enters this branch. This is a local rehearsal trap only — but
rehearsing in a throwaway clone is the prescribed verification route for the release
machinery (#9497, #9500 and #9518 all call for it), so it is on the path of every card that
touches this area.
Adjacent, different mechanism: #9408 covers a shallow objectui clone defeating
bump-objectui.sh's walkability guard.
Shape of a fix
Options, roughly in increasing cost: document the scaffold above wherever the throwaway-clone
rehearsal is prescribed; or provide it as a small script next to the other scripts/pm/
helpers; or have the rehearsal recipe detect git rev-parse --is-shallow-repository and
refuse with this explanation rather than hanging.
Generated by Claude Code
Found while rehearsing #9518's fix. Cost roughly two and a half hours of wall clock across two attempts before it was diagnosed. Recording so the next rehearsal — #9500 is the scheduled one — does not pay it again.
What happens
changeset versionresolves a commit for every consumed changeset to build changeloglinks.
@changesets/git'sgetCommitsThatAddFilesdoes this per path:and then branches on whether the returned commit has a parent. A parentless commit is
treated as possibly being the boundary of a shallow clone rather than the real add, so:
git rev-parse --is-shallow-repositoryis true, it callsdeepenCloneBy({ by: 50 })(
git fetch --deepen=50) and retries those paths;Why it never terminates here
The agent container's
/home/user/objectstackis itself a shallow clone. Any clonetaken from it inherits
.git/shallow, and every.changeset/*.mdresolves to the shallowboundary commit, which by definition has no parent. The rehearsal recipe also says to use a
throwaway clone with no push remote — so
git fetch --deepen=50has no remote to fetchfrom,
remainingnever shrinks, and thedo ... while (remaining.size)loop spins forever.Observable symptom:
changeset versionnever finishes,git statusstays clean, and thesame changeset filenames scroll past repeatedly in
ps. It looks like slow progress, not ahang, which is what makes it expensive — the first attempt was allowed to run 70 minutes on
the assumption that O(packages x changesets) was simply large.
Two dead ends worth naming: deleting
.git/shallowmakes git fail outright(
cannot simplify commit ... because of ...on the missing parent), and re-adding a remoteto the shallow source cannot help either, because deepening from a shallow source still
lands on a parentless boundary.
What does work
Give every changeset an add-commit that has a parent, inside the throwaway clone, before
running the version pass. The net tree is unchanged (verified: identical tree hash):
A full resolution pass then takes 0.57s instead of never completing, and
pnpm run versionruns end to end.Not a production defect
cut-rc.ymlchecks out withactions/checkout@v7andfetch-depth: 0, so the real cut hasfull history and never enters this branch. This is a local rehearsal trap only — but
rehearsing in a throwaway clone is the prescribed verification route for the release
machinery (#9497, #9500 and #9518 all call for it), so it is on the path of every card that
touches this area.
Adjacent, different mechanism: #9408 covers a shallow
objectuiclone defeatingbump-objectui.sh's walkability guard.Shape of a fix
Options, roughly in increasing cost: document the scaffold above wherever the throwaway-clone
rehearsal is prescribed; or provide it as a small script next to the other
scripts/pm/helpers; or have the rehearsal recipe detect
git rev-parse --is-shallow-repositoryandrefuse with this explanation rather than hanging.
Generated by Claude Code