| name | npm-release-workflow |
| description | Use when bumping npm package versions, publishing prereleases, promoting to stable, or reasoning about this repo's GitHub Actions-based npm release flow |
NPM Release Workflow Skill
Purpose
Guide agents through the correct process for bumping versions, publishing prereleases (alpha/beta/rc), and promoting to stable in this repository, where npm publication is normally performed remotely by GitHub Actions after a version bump is pushed to main. Prevent the most common npm publishing mistakes, especially bypassing the repo's publish workflow.
Semantic Versioning Quick Reference
Version Format
MAJOR.MINOR.PATCH-PRERELEASE.N
│ │ │ │ └─ prerelease iteration (0, 1, 2, …)
│ │ │ └─ prerelease identifier (alpha, beta, rc)
│ │ └─ backwards-compatible bug fixes
│ └─ backwards-compatible new features
└─ breaking API changes
Prerelease Stages
| Stage | Purpose | Audience |
|---|
| alpha | Active development, API unstable | Internal devs only |
| beta | Feature-complete, API stabilizing | Trusted testers, early adopters |
| rc | Production-ready candidate | Broader community |
| stable | Production release | Everyone |
When To Use Each Bump
| Situation | Command |
|---|
| Bug fix, no API change | npm version patch |
| New feature, backwards-compatible | npm version minor |
| Breaking API change | npm version major |
| Enter prerelease for next minor | npm version preminor --preid=alpha |
| Enter prerelease for next major | npm version premajor --preid=alpha |
| Iterate within a prerelease | npm version prerelease --preid=alpha |
| Transition alpha → beta | npm version prerelease --preid=beta or npm version X.Y.Z-beta.0 |
| Promote prerelease to stable | npm version patch (strips suffix, does NOT bump number) |
Critical Rules (Non-Negotiable)
Rule 1: Treat GitHub Actions as the canonical publish path
In this repository, the normal release path is:
- Update
CHANGELOG.md
- Bump
package.json with npm version ...
- Push the version commit to
main
- Let
.github/workflows/publish.yml decide whether to publish and which dist-tag to use
Do not use a local npm publish as the default release mechanism. That bypasses the repository's version-check logic, prerelease tag detection, and remote audit trail.
Rule 2: NEVER publish a prerelease without --tag
npm publish
npm publish --tag alpha
npm publish --tag beta
npm publish --tag rc
npm publish ALWAYS sets the latest dist-tag unless --tag is specified. This is the single most common npm publishing mistake.
Rule 3: Always run quality gates before pushing a release bump
At minimum: lint, test, build. Never trust that tests were run locally.
npm run lint && npm test && npm run build
Rule 4: Keep git tags in sync with package.json versions
npm version creates both a commit and a git tag by default. Do not suppress this unless you have an explicit reason. Always push tags:
git push --follow-tags
Rule 5: Clean up dist-tags after promoting to stable
Stale prerelease tags confuse users. After a stable release:
npm dist-tag rm <package> alpha
npm dist-tag rm <package> beta
npm dist-tag rm <package> rc
Version Progression Examples
Full Prerelease Cycle
npm version preminor --preid=alpha
npm version prerelease --preid=alpha
npm version prerelease --preid=alpha
npm version prerelease --preid=beta
npm version prerelease --preid=beta
npm version prerelease --preid=rc
npm version prerelease --preid=rc
npm version minor
Hotfix While in Prerelease
If you need to patch the current stable while a prerelease is in flight, cut the hotfix from the branch that will be merged back to main, bump the patch version there, and let publish.yml publish latest after that hotfix reaches main. If your team uses a dedicated release branch, merge it back before relying on the repo's canonical publish workflow.
The prerelease vs prepatch Distinction
prerelease: If already in a prerelease, increments ONLY the prerelease number (1.2.0-alpha.0 → 1.2.0-alpha.1). This is the command for iterating.
prepatch: Always bumps the patch version AND starts a new prerelease (1.2.0-alpha.2 → 1.2.1-alpha.0). This is for entering a new prerelease cycle.
Finalizing a Prerelease
When the current version is a prerelease, npm version patch strips the suffix without incrementing:
npm version patch
This is correct because 1.2.0-rc.5 is semver-less-than 1.2.0.
Dist-Tag Management
Standard Tags
| Tag | Meaning | Consumer command |
|---|
latest | Current stable (default) | npm install <pkg> |
alpha | Alpha prerelease | npm install <pkg>@alpha |
beta | Beta prerelease | npm install <pkg>@beta |
rc | Release candidate | npm install <pkg>@rc |
next | Catch-all for any prerelease | npm install <pkg>@next |
latest is the ONLY tag with semantic meaning to npm — npm install <pkg> resolves to it. All other tags are conventions.
Inspecting Tags
npm dist-tag ls <package>
npm view <package> dist-tags
Recovering from a Bad Publish
If a prerelease accidentally became latest:
npm dist-tag add <package>@<stable-version> latest
Canonical Repository Release Workflow
Preflight Checklist (Agent Must Verify Before Pushing)
- Working tree is clean (
git status --porcelain returns empty)
- All tests pass (
npm test)
- Lint passes (
npm run lint)
- Build succeeds (
npm run build)
- CHANGELOG.md is updated with the new version entry
- You are on the correct branch (typically
main)
Step-by-Step: Prerelease
git status --porcelain
npm run lint && npm test && npm run build
git add CHANGELOG.md
git commit -m "docs(changelog): update for X.Y.Z-stage.N"
npm version prerelease --preid=alpha -m "chore(release): %s"
git push origin main --follow-tags
Step-by-Step: Stable Release
git status --porcelain
npm run lint && npm test && npm run build
git add CHANGELOG.md
git commit -m "docs(changelog): update for X.Y.Z"
npm version <patch|minor|major> -m "chore(release): %s"
git push origin main --follow-tags
npm dist-tag rm <package> alpha 2>/dev/null
npm dist-tag rm <package> beta 2>/dev/null
npm dist-tag rm <package> rc 2>/dev/null
What Actually Triggers Publication In This Repo
publish.yml runs on pushes to main and on manual workflow dispatch. The workflow:
- Reads
package.json version as the target version
- Checks the currently published npm version
- Skips the publish job entirely when the versions already match
- Detects
alpha, beta, and rc prerelease identifiers from the version string
- Runs
npm run build and npm test only when publication is needed
- Publishes remotely with
npm publish --access public --tag <detected-tag>
CI Publishing Workflow (GitHub Actions)
Recommended: Version-Check Pattern
This repo uses a push-to-main trigger that compares package.json version against the npm registry. A version bump committed and pushed to main is the normal way to publish. The workflow auto-detects prerelease versions, sets the correct dist-tag, and skips publication when npm already has the same version.
Key CI logic for dist-tag detection:
VERSION=$(node -p "require('./package.json').version")
if echo "$VERSION" | grep -qE '-(alpha|beta|rc)\.'; then
TAG=$(echo "$VERSION" | sed -E 's/.*-(alpha|beta|rc)\..*/\1/')
else
TAG="latest"
fi
npm publish --access public --tag "$TAG"
Alternative: Tag-Triggered Pattern
For repos that prefer explicit control, trigger CI on git tag push:
on:
push:
tags:
- 'v*'
CI should then verify that the git tag matches package.json version:
GIT_TAG=${GITHUB_REF#refs/tags/v}
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$GIT_TAG" != "$PKG_VERSION" ]; then
echo "::error::Tag $GIT_TAG doesn't match package.json $PKG_VERSION"
exit 1
fi
Trusted Publishing (OIDC — Recommended for New Setups)
npm supports tokenless publishing via OIDC as of npm CLI ≥ 11.5.1 / Node ≥ 22.14.0:
- Configure on npmjs.com → Package Settings → Trusted Publisher → GitHub Actions
- Add
permissions: { id-token: write } to the workflow
- Remove
NPM_TOKEN secret — not needed with OIDC
- Provenance attestation is automatic
If using traditional NPM_TOKEN:
- Store in GitHub repository secrets
- Use automation tokens (bypass 2FA)
- Rotate periodically
- Never commit to source
Post-Publish Verification (Agent Must Perform)
npm view <package> dist-tags
npm view <package> versions --json
Using the Existing Release Script
This repo has scripts/release.mjs which:
- Ensures clean working tree
- Runs lint → test → clean → build → pack
- Exits after local validation with instructions to push the version bump to
main
npm run release
Important: In this repository, npm run release is a validation helper only. Push the version bump to main with git push origin main --follow-tags to trigger publish.yml for the actual npm publish.
Decision Tree for Agents
Is this a prerelease?
├─ YES → Which stage?
│ ├─ First prerelease of a new version
│ │ └─ npm version pre{major|minor|patch} --preid={alpha|beta|rc}
│ ├─ Iterating within same stage
│ │ └─ npm version prerelease --preid={alpha|beta|rc}
│ └─ Transitioning stages (e.g. alpha → beta)
│ └─ npm version prerelease --preid={new-stage}
│ OR npm version X.Y.Z-{new-stage}.0
│
└─ NO → Stable release
├─ Currently on a prerelease version?
│ ├─ YES → npm version patch (strips suffix)
│ └─ NO → npm version {patch|minor|major}
└─ After publish: clean up stale dist-tags
Common Mistakes This Skill Prevents
| Mistake | Prevention |
|---|
| Local publish bypasses repo workflow | Treat push-to-main + publish.yml as the canonical release path |
Prerelease published as latest | CI auto-detects preid and uses --tag |
| Tests not run before release push | Preflight and CI both run validation before remote publish |
| Git tag missing | npm version creates tags by default |
| Re-publishing an existing version | CI compares registry version and skips when unchanged |
| Stale dist-tags after stable release | Checklist includes dist-tag cleanup |
| CHANGELOG not updated | Preflight checklist requires it |
| Dirty working tree at release | Release script checks git status --porcelain |
| Publishing from wrong branch | Canonical workflow requires the version bump to reach main |
Lifecycle Script Hooks (Optional Enhancement)
For repos that want automated pre-publish validation in package.json:
{
"scripts": {
"preversion": "npm run lint && npm test",
"version": "npm run build && git add -A dist",
"postversion": "git push --follow-tags"
}
}
This makes npm version automatically: run tests → build → commit → tag → push.
References