Motivation
The current .github/workflows/release.yml runs check → github-release → publish-npm as one tag-triggered pipeline. When check fails (or any later step fails before publish-npm), recovery requires force-moving the tag — fine when npm publish hasn't run, but fragile and easy to misuse.
The 0.1.1 release surfaced this concretely: the check job failed because source/cli.ts hardcoded '0.1.0' while package.json had been bumped to 0.1.1 by npm version patch. Recovery was a force-pushed tag (git tag -f v0.1.1; git push --force origin v0.1.1). That worked because publish-npm was correctly gated behind check's success — but the entire path is opaque to a future maintainer.
Proposal
Two separate pipelines, with the irreversible step gated behind explicit human / state confirmation:
-
release-github.yml — triggered by tag push (v*).
- Runs
npm ci, typecheck, test, build.
- Creates the GitHub Release with auto-generated notes.
- Stops here. Reversible: GitHub Releases can be deleted; tags can be moved.
-
release-npm.yml — triggered by GitHub Release published event (manually-promoted, OR auto-promoted only when the release is non-prerelease).
- Re-runs
npm ci, typecheck, test, build (defensive — re-validate on a fresh runner).
- Publishes to npm with provenance via OIDC.
- Irreversible: npm publish is one-way (unpublish has 72h window + deprecation flags).
The split has two benefits:
- Observability: a release that fails before npm publish leaves a clean GitHub Release artifact + failed pipeline. The tag already exists; the maintainer can move it forward and re-run, or delete the GH Release and the tag, without npm interference.
- Safety net: even with auto-promotion, the trigger boundary (Release
published event) gives a clear rollback point. gh release delete v0.1.X on a botched release stops the npm publish from firing if it hasn't yet.
Open questions
- Should the npm publish trigger be the Release event auto-fire, or require an explicit label / dispatch? Auto-fire matches today's behavior; explicit dispatch is safer but adds a manual step.
- Pre-release handling: tags like
v0.2.0-beta.1 should publish to npm under the next dist-tag, not latest. Encode in the npm pipeline.
- Provenance: confirm OIDC trusted publishing still works across two workflows (it should — the npm publish step is identical to today's, just in a different file).
Severity
Not urgent. The current pipeline works for happy-path releases and force-tag-move recovery handles the failure case. Prioritize before v0.2.0 ships, when the release frequency picks up.
Filed during the 0.1.1 hotfix release after the maintainer flagged the pattern as worth automating.
Motivation
The current
.github/workflows/release.ymlrunscheck → github-release → publish-npmas one tag-triggered pipeline. Whencheckfails (or any later step fails beforepublish-npm), recovery requires force-moving the tag — fine whennpm publishhasn't run, but fragile and easy to misuse.The 0.1.1 release surfaced this concretely: the
checkjob failed becausesource/cli.tshardcoded'0.1.0'whilepackage.jsonhad been bumped to0.1.1bynpm version patch. Recovery was a force-pushed tag (git tag -f v0.1.1; git push --force origin v0.1.1). That worked becausepublish-npmwas correctly gated behindcheck's success — but the entire path is opaque to a future maintainer.Proposal
Two separate pipelines, with the irreversible step gated behind explicit human / state confirmation:
release-github.yml— triggered by tag push (v*).npm ci,typecheck,test,build.release-npm.yml— triggered by GitHub Releasepublishedevent (manually-promoted, OR auto-promoted only when the release is non-prerelease).npm ci,typecheck,test,build(defensive — re-validate on a fresh runner).The split has two benefits:
publishedevent) gives a clear rollback point.gh release delete v0.1.Xon a botched release stops the npm publish from firing if it hasn't yet.Open questions
v0.2.0-beta.1should publish to npm under thenextdist-tag, notlatest. Encode in the npm pipeline.Severity
Not urgent. The current pipeline works for happy-path releases and force-tag-move recovery handles the failure case. Prioritize before v0.2.0 ships, when the release frequency picks up.
Filed during the 0.1.1 hotfix release after the maintainer flagged the pattern as worth automating.