The release pipeline (.github/workflows/release.yml + GoReleaser v2)
fires automatically on a v*.*.* tag push. This checklist is the
human-side companion for shipping any release — including patch
releases off main. The first nodeupx@1.0.1 bootstrap publish was
the only manual step ever required and is preserved below for
historical reference.
- All planned issues / PRs for this version are merged to
main -
CHANGELOG.mdregenerated from conventional commits - README install instructions verified against actual artifacts
- All docs (
docs/*.md) reviewed for accuracy -
make cigreen locally - Cross-platform smoke test:
-
nodeup checkon macOS (arm64) -
nodeup checkon Linux (amd64) -
nodeup checkon Windows (amd64) [if Windows runner available]
-
- Manual upgrade flow tested on at least one platform
- GitHub Action secrets present:
-
HOMEBREW_TAP_TOKEN -
SCOOP_BUCKET_TOKEN -
CODECOV_TOKEN(optional)
-
git checkout main
git pull
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.Z- GitHub Release visible at /releases/tag/vX.Y.Z
- All 5 binaries attached + checksums.txt
- Homebrew cask pushed to
dipto0321/homebrew-tap - Scoop manifest pushed to
dipto0321/scoop-bucket -
publish-npmjob green;npm view nodeupx versionmatches the tag (automatic via OIDC Trusted Publishing — see below; manual for the very firstnodeupxpublish only)
npm registry propagation. The publish job going green does not mean the version is visible on npmjs.com yet — the registry takes a few minutes to propagate. Poll
npm view nodeupx versionuntil it matches the tag before you verify the install path, or you'll chase a phantom failure.
The postinstall script is the part that actually breaks for users, so
verify it against the published package, not the local checkout.
Set VERSION to the tag you're shipping (without the v):
VERSION=1.2.4
# 1. Confirm the published metadata.
npm view nodeupx version dependencies engines
# → version $VERSION, dependencies { tar: '7.5.22' }, engines { node: '>=18' }
# 2. Install into a scratch dir and audit.
mkdir -p /tmp/nodeupx-check && cd /tmp/nodeupx-check && npm init -y
npm install nodeupx@$VERSION
npm audit # → "found 0 vulnerabilities"On npm 12+ the install finishes with the wrapper in place but
no binary — npm's allowScripts gate blocks the postinstall
unless the package is approved:
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts nodeupx@1.2.4 (preinstall: node scripts/check.js; postinstall: node scripts/install.js)
Approve it and re-install, then confirm the binary landed and runs:
npm install-scripts approve nodeupx
npm install nodeupx@$VERSION
ls -l node_modules/nodeupx/bin/nodeup
xattr -p com.apple.quarantine node_modules/nodeupx/bin/nodeup # → no such xattr
node_modules/nodeupx/bin/nodeup versionThe quarantine check matters: the wrapper strips the attribute itself,
so a binary that carries it would be killed by macOS before main().
nodeup shipped as a Homebrew formula up to v1.2.1. GoReleaser
deprecated brews: in v2.10 in favour of homebrew_casks:, so v1.2.2
ships a cask instead — a cask's binary: stanza is declarative,
while a formula's install: block is raw Ruby, and Homebrew now
prefers casks for "download a binary and put it on PATH" distributions.
Homebrew treats a formula and a cask of the same name as two
different packages, so the old formula has to be removed from the tap
by hand, and the tap needs a tap_migrations.json so Homebrew can
perform the hand-off automatically on brew update.
The file is a flat {old_name: new_name} hash (not a nested
{"Formula": {...}} object — Homebrew's Tap#tap_migrations parses
it with JSON.parse(...read) and looks the name up directly):
{
"nodeup": "nodeup"
}-
tap_migrations.jsonpresent indipto0321/homebrew-tapwith the flat{"nodeup": "nodeup"}mapping - The old
nodeup.rbformula removed from the tap (GoReleaser pushes the cask but does not delete the formula) - The formula removal, the
tap_migrations.json, and the cask all land in the same update cycle — with the formula still present,brew install dipto0321/tap/nodeupresolves to the formula (Homebrew prints "Treating ... as a formula"), so the cask's quarantine-stripping hook would never reach users -
brew install dipto0321/tap/nodeupverified on a scratch tap - Existing formula installs get the hand-off on
brew update(Homebrew unlinks the old keg, installs the cask, and tells the user tobrew uninstall --formula --force nodeup)
Checksum trap. Do not hand-edit the sha256 values in the cask from a local
goreleaser --snapshotrender — Go builds are not byte-reproducible, so the snapshot hashes will not match the published release assets and the install fails with "Cask nodeup reports different checksum". Generate the cask from the release's ownchecksums.txt, or let GoReleaser push it at tag time (which is what actually happens for every release after v1.2.2).
The npm wrapper lives in nodeup-npm/ at the repo root. Publishing
happens after the GitHub release is up: the wrapper's
postinstall script (scripts/install.js) fetches the binary
matching the binaryVersion field in package.json, so the GitHub
release for that version must already exist — otherwise every
install 404s on the binary download.
The decided flow is OIDC Trusted Publishing. The publish-npm
job in .github/workflows/release.yml is already wired: on every
v*.*.* tag push it runs after GoReleaser, exchanges the runner's
OIDC token for a one-hour publish token scoped to nodeupx, and
runs npm publish --provenance --access public. No NPM_TOKEN
secret, nothing to rotate. From the second publish onward there is
no manual npm step at all — tag, wait, verify.
The only manual publish is the first one: npm's Trusted Publisher config lives on the package's access page, which doesn't exist until the package does. So the one-time bootstrap is:
Account prerequisites:
- An
npmjs.comaccount. The barenodeupname is owned by an unrelated 2015 package (romanmt/nodeup, "a simple cluster implementation for node") so we ship undernodeupx(unclaimed as of 2026-07-04; the earliernodeup-cli@1.0.0was unpublished). - 2FA enabled with an authenticator-app factor (TOTP), under
https://www.npmjs.com/settings/<your-username>/security→ Two-Factor Authentication → Authenticator app. SMS and email-only are not accepted for publish. Save the recovery codes in your password manager.
# 1. Sanity-check the tarball. This should print exactly 5 files:
# LICENSE, README.md, package.json, and the two scripts.
# No bin/, no .npmignore, no node_modules.
cd nodeup-npm
npm pack --dry-run
# 2. Log in. Since the Dec 2025 token changes, `npm login` produces
# a short-lived session token (~2 hours); it prompts for an OTP
# from your authenticator app.
npm login
# 3. Publish. npm prompts for a fresh OTP at publish time (separate
# from the login OTP) — keep your authenticator open.
npm publish
cd ..Then register the trusted publisher so every later version ships from CI:
- Open https://www.npmjs.com/package/nodeupx/access (or the
"Trust" tab on the package page) and add a trusted publisher:
- Provider: GitHub Actions
- Repository:
dipto0321/nodeup - Workflow:
release.yml - Environment: (leave blank — this repo doesn't use one)
- Nothing to change in the repo —
release.ymlalready carries thepublish-npmjob (id-token: write, clean.npmrcwith no_authTokenline, Node 24 for npm ≥ 11.5.1). See the job's comments for the OIDC footguns it deliberately avoids.
Automatic. The tag push runs GoReleaser, then publish-npm
publishes the wrapper via OIDC with --provenance (npm shows the
"Built and signed on GitHub Actions" badge). Verify with
npm view nodeupx version. If the job fails with ENEEDAUTH,
check that the trust entry on npmjs.com still matches this repo,
the release.yml filename, and the GitHub user — that trio is the
whole auth surface.
Token-based fallback (not used here; only if publishing from a
non-GitHub CI): a granular access token, package-scoped to
nodeupx, 90-day max expiry, stored as NPM_TOKEN and passed as
NODE_AUTH_TOKEN to npm publish. Prefer the trusted publisher —
it leaves no secret to rotate or leak.
- GitHub Discussion / announcement posted
- Any release-blocker issues filed as follow-up
-
mainbranch advanced to next version in CHANGELOG.md