publish: a release you can resume, and announce last - #98
Merged
Merged
Conversation
Run 33854645746 published the library, failed on the demo's build, and left
a public v0.1.10 release for a version nobody could install in full. Retrying
the whole workflow hit the already-published library, which npm refuses, so
0.1.10 was abandoned for 0.1.11.
Two irreversible uploads cannot be made atomic, so they are resumable instead.
scripts/publish.mjs splits the release into pack and publish:
* pack builds both tarballs before either is uploaded and records their
bytes, so a build failure happens while nothing is public. It refuses a
tarball missing its build, checking every path package.json promises and
then following the relative imports inside the tarball, which is what
catches a demo packed without a dist.
* publish asks the registry about each version first. A verified match is
skipped, an absent version is uploaded, and a different artifact under the
same version or an unreadable registry fails before anything is uploaded.
Sameness is decided on contents, not gzip bytes, so a repack of the same
commit is recognised rather than fought over.
* publish then waits for both versions to be readable before exiting zero.
The workflow announces last. A tag push does the registry work and turns a
waiting draft into the release, because a draft cannot start it: GitHub does
not trigger workflows for the created activity type on drafts. Publishing a
release by hand still works and still publishes both packages.
The dist-tag now comes from the version, so a prerelease goes out as next and
its release is marked as one. The packed tarballs are kept as a run artifact
to resume a failed run from by hand.
allowJs lets the library's tsconfig type the test's import of the script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm pack --json` needs its stdout captured; `npm publish` needs its stdout in the log, where a person can read what was uploaded. One helper was doing both, so the publish output was swallowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #93.
What went wrong
Run 33854645746 published
@profullstack/hqtui@0.1.10, failed in the demo's build, and left the v0.1.10 release public for a version nobody could install in full. Retrying the workflow reached the already-published library first, which npm refuses, so recovery burned 0.1.11.publish.ymlstill ran two unconditionalnpm publishsteps after arelease: publishedtrigger, so any later registry, network or auth failure reproduces the same split state.What this does
scripts/publish.mjssplits the release intopackandpublish, following the three steps the issue asked for.1. Build and pack both packages before either upload, and keep the artifacts. Both tarballs are packed first, so a build failure happens while nothing is public, and they are uploaded as a run artifact (
release-<tag>) before the firstnpm publishso a failed run can be resumed from the exact bytes.packalso refuses a tarball that is missing its build. It checks every pathpackage.jsonpromises, then follows the relative imports inside the tarball —apps/demo's entry point is one line,import "../dist/main.js", so the declared entry point can be present in a tarball that installs nothing runnable. Removing thedistand packing reproduces the 0.1.10 failure shape:2. Distinguish a verified match from an absent version. Every package is looked up before anything is uploaded, and the answer is one of three states, never a guess: absent, present with a shasum, or indeterminate. Absent is published, a verified match is skipped, and a conflict or an unreadable registry fails the run before any upload. An unreachable registry read as "absent" is how you publish twice; read as "present" is how you skip a package that never shipped.
Sameness is decided on contents rather than gzip bytes: if
dist.shasumdiffers, the published tarball is downloaded and compared file by file, because two packs of one commit can differ in packing without differing in what they install. Only a real content difference is a conflict.3. Verify both packages before finalizing the release.
publishwaits for both versions to be readable, and the GitHub release is announced last.A draft cannot start the workflow — GitHub does not trigger workflows for the
createdactivity type on draft releases — so the tag push is the way in: write the notes as a draft, push the tag, and the workflow flips that draft to public once npm has both packages. With no draft it creates the release from the commit log. Publishing a release by hand still works and still publishes both packages; it just announces first, which is the ordering that made this possible.Also: the dist-tag now comes from the version, so a prerelease publishes as
nextand its release is marked as one, instead of failing the guard on the release path.Tests
packages/hqtui/test/publish.test.ts, 21 tests under both bun andnode --test, with the registry and npm as functions. Including the issue's regression scenario:Verified against the real tree and the live registry:
packproduces both tarballs with no false positives from the new guards, and a dry-runpublishcorrectly classifies@profullstack/hqtui-demo@0.6.3as an identical skip while flagging the library as changed since it was published (#94 touchedui.tsafter that publish).Full suite: 426 tests pass on bun and Node,
bun run typecheckclean.docs/RELEASING.mdwrites down the flow and the recovery, which is now "re-run the failed run".Unrelated observation while testing:
0.6.3is on npm but has nov0.6.3tag or GitHub release.🤖 Generated with Claude Code