| name | release-engineering |
| description | Version, changelog, tag, and publish a release. Use when cutting a release, setting up automated publishing to npm/PyPI/crates.io/Maven/Homebrew, deciding whether a change is major/minor/patch, writing release notes, adopting conventional commits or changesets, planning a 1.0, or handling a bad release that needs to be yanked. Covers semver decision rules, changelog quality, release automation with provenance, pre-releases, LTS branches, and deprecation timelines. |
Release Engineering
A release is a promise. Semver is the notation for that promise, and users treat it
literally — a breaking change in a patch release breaks builds at 3 a.m. for people who
did nothing wrong.
Semver, decided correctly
MAJOR.MINOR.PATCH — increment MAJOR for incompatible changes, MINOR for
backward-compatible additions, PATCH for backward-compatible fixes.
The judgment calls, resolved:
| Change | Bump | Note |
|---|
| Add an optional parameter | minor | |
| Add a required parameter | major | |
| Add a new export | minor | |
| Remove or rename any export | major | Deprecate first (api-design) |
| Change a default value | major | Behavior changes silently — the worst kind |
| Fix a bug users may depend on | major in practice | Hyrum's Law; judge by blast radius |
| Add a new error type | minor | Unless callers must handle it exhaustively |
| Change an error message | patch | Unless documented as stable |
| Change an error type | major | |
| Tighten input validation | major | Previously-working input now fails |
| Loosen validation | minor | |
| Raise minimum runtime version | major | Node 18→20, Python 3.9→3.10 |
| Add a required peer dependency | major | |
| Bump a dependency's major | major if its types are in your public API, else minor | |
| Performance improvement | patch/minor | major if a complexity guarantee changes |
| Docs, tests, CI | none | No release needed |
| Change output ordering | major if previously stable | Someone parses it |
0.x is not an exemption from thinking. Convention: 0.MINOR.PATCH where minor
carries breaking changes. But once real users exist, communicate breaks regardless of
what the version number technically permits.
1.0 means "the API is stable and I will honor semver." Ship it once that is true.
Sitting at 0.x for years while enterprises depend on you is worse than an honest 1.0 —
0.x is read by procurement as "not production ready", and by dependency tooling as
"pin exactly".
Changelogs humans read
CHANGELOG.md in Keep a Changelog form. Newest first,
grouped by kind, written for the person deciding whether to upgrade.
## [4.2.0] - 2026-03-14
### Added
- `--watch` mode re-runs on file change ([#412](link)) — thanks @contributor!
### Changed
- Default timeout raised 5s → 30s. Set `timeout: 5000` to restore. ([#420](link))
### Fixed
- Crash on config files containing a BOM ([#418](link))
### Deprecated
- `parse()` — use `parseConfig()`. Removed in v5.0.0. ([migration guide](link))
Rules:
- Write for the upgrader, not the committer. "Refactored the visitor" tells them
nothing; "20% faster on large files, no API change" does.
- Every entry links to its PR or issue.
- Credit contributors by handle. This is a large part of what people get paid in.
- Breaking changes get their own section, at the top, with the migration action
inline. Not a link to a link.
- Say what to do, not only what changed.
- Never auto-dump commit subjects. A changelog of
fix: stuff and chore: bump
is noise that trains users not to read it.
Auto-generation (release-please, semantic-release, changesets) is fine as a draft.
Edit it before publishing.
Automation
Pick one model and commit to it:
| Approach | How it decides the version | Best for |
|---|
| Conventional commits + semantic-release | Parses commit messages | Single-package repos, disciplined commit history |
| release-please | Commits → maintains a release PR | Same, with a human approval gate |
| changesets | Contributor writes a .changeset/ file in their PR | Monorepos; intent is captured by the person who knows it |
| Manual | You decide | Small projects, or where judgment matters more than throughput |
Changesets has a real advantage for community projects: the contributor states the
impact of their own change at PR time, when the context is fresh, and the maintainer
reviews that claim as part of review.
If using conventional commits, enforce them on the PR title rather than every commit
(squash-merge makes the title the commit message). Rejecting a first-time
contributor's commit message format is a bad first interaction; a maintainer editing
the title at merge is free.
Release workflow
name: Release
on:
push:
tags: ['v*.*.*']
permissions:
contents: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', registry-url: 'https://registry.npmjs.org' }
- run: npm ci
- run: npm test
- run: npm publish --provenance --access public
Non-negotiables:
- Publish from CI, never from a laptop. Laptop publishes ship uncommitted files,
wrong branches, and stale builds — and they are unattestable.
- Use trusted publishing / OIDC where available (npm, PyPI, crates.io). No
long-lived registry token to leak.
- Emit provenance attestations (
--provenance, PyPI attestations). See
supply-chain-security.
- Tests must pass in the release job, not just "they passed on main last week".
- Use a protected GitHub Environment so publishing requires an approval click.
Pre-1.0, pre-releases, and channels
4.0.0-rc.1 for release candidates; publish to a next dist-tag so nobody
installs it accidentally. Ask specific downstream users to try it.
4.0.0-beta.3 when the API may still shift.
- Nightly/canary builds from
main for projects with eager early adopters — but
only if you will actually respond to bugs found there.
- LTS branches once enterprises depend on you: maintain
3.x with security and
critical fixes for a stated window, publish the support matrix and the end-of-life
dates in the README. Stating a date you will honor is worth more than a longer
window you will abandon.
The release checklist
[ ] All milestone issues closed or moved
[ ] CI green on main, on the full matrix
[ ] CHANGELOG updated and human-edited
[ ] Version bumped in every manifest (package.json, Cargo.toml, __init__.py, ...)
[ ] Docs updated; migration guide written if there are breaking changes
[ ] README install commands re-verified against the new version
[ ] Deprecation warnings added for anything being removed next major
[ ] Tag signed and pushed: git tag -s v4.2.0 -m "v4.2.0" && git push origin v4.2.0
[ ] GitHub Release created with notes and binaries/checksums
[ ] Package published with provenance
[ ] Install verified from the registry in a clean container
[ ] Announced where your users actually are
That second-to-last line catches more real breakage than any other check:
docker run --rm -it node:22 sh -c 'npm i -g yourpkg@4.2.0 && yourpkg --version'
docker run --rm -it python:3.13 sh -c 'pip install yourpkg==4.2.0 && python -c "import yourpkg"'
Missing files in the published artifact (files in package.json,
MANIFEST.in, include in Cargo.toml) is the most common release bug and it is
invisible from inside the repo.
When a release is bad
Speed matters more than dignity.
- Assess. Broken install or data loss → act now. Cosmetic → fix forward.
- Deprecate the version on the registry — do not delete it.
npm deprecate pkg@4.2.0 "Broken: use 4.2.1", yank on crates.io, PyPI "yank". Deleting breaks
every lockfile that already references it, including for people who were fine.
(npm unpublish is only permitted within 72 hours and should still be a last resort.)
- Publish the fix as a new patch version. Never re-publish a version number.
- Never move a tag. People have it cached; a moved tag makes verification fail
and looks like an attack.
- Pin the issue, update the release notes with a warning, and post where you
announced it.
- Write the postmortem into the checklist. The failure means a check is missing.
Anti-patterns
- Breaking changes in a patch release. The trust-destroying mistake.
- No changelog, or a changelog of raw commit subjects.
- Publishing from a laptop.
- Long-lived registry tokens in repo secrets when OIDC is available.
- Unsigned, unattested artifacts for anything widely installed.
- Version numbers that disagree across manifest, tag, and changelog.
- Deleting a published version. Yank or deprecate instead.
- Sitting at 0.x with 500k weekly downloads.
- A 1.0 that is followed by 2.0 six weeks later. 1.0 is a commitment; earn it first.
- Releasing on a Friday afternoon with nobody around to respond.