Pushing a release pushes the branch and then the tags seconds apart.
publish-snapshot.yml keys its concurrency group on the branch and
release.yml keys its own on the tag, so the two never gate each other —
on a single runner they race for it and the branch push always wins.
On 1.0.1 the snapshot job wedged extracting a layer of the runner image,
the release job never started, release.mjs gave up after its 20-minute
wait, and two of three tags were left unpushed. The report still printed
a bold "Done" above an empty shipped list, so it read as a success.
- publish-snapshot.yml skips commits whose message starts with "release:".
A snapshot of a release commit is the same tree the tag is about to
publish properly, so skipping costs nothing and removes the race.
- waitForRelease() offers to keep waiting instead of giving up. No
timeout value survives a wedged runner, so the real choice is between
asking and making the operator finish the release by hand. --yes and a
non-interactive run still give up; the latter matters because confirm()
answers with its default without a terminal, which would extend the
deadline forever.
- The final header says "Blocked" when it is, and labels the packages
that did ship before the blockage.
- --wait-timeout defaults to 2400s rather than 1200s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
`pnpm run release` (scripts/release.mjs, zx + enquirer + commander) replaces
the hand sequence of bump, changelog, gate, tag, push. It picks packages from a
list annotated with what npmjs already has, computes versions from the manifest,
collects notes in $EDITOR seeded with the commits since the package's last tag,
and prepends them to CHANGELOG.md in the format release.yml's parser expects.
The gate (lint:ci -> typecheck -> test:coverage -> build -> verify-pack) runs
against the bumped tree *before* the commit, so a failure leaves nothing to
unpick -- it offers to restore instead. Tags go out dependency-first, and each
version is polled on npmjs before the next tag is pushed.
That polling is what lets release.yml lose its `check linked deps are released`
step: the ordering is now enforced before CI ever sees a tag, rather than after.
linked-deps.mjs stays as a hand-check. The accepted cost is that a tag pushed
some other way is no longer caught.
Three things found by running it rather than reading it:
- Tags are annotated (`-a -m`). A lightweight tag is rejected outright under
tag.forceSignAnnotated, which is set on the machine this was written on.
- pnpm 11 forwards the `--` in `pnpm run release -- --dry-run` literally, and
commander reads a bare `--` as "the rest are positionals". The script takes no
positionals, so it strips it and both spellings work.
- Prompts refuse with a message naming the flag that avoids them when stdin is
not a TTY, instead of hanging as an unsettled top-level await.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ