A powerful release script that automates the entire release process including updating changelog, npm publishing (using pnpm) and GitHub releases.
Publishing a new version of a package is a routine sequence of several steps involving running commands, editing files, entering passwords, copying text to GitHub, and so on. It's easy to make a mistake at any stage, especially when editing CHANGELOG.md. Do it all with a single command or push to the release branch.
-
In GitHub CI
- Create a release action. See the
release.yamlfile for an example, where important points are described in the comments. - In the settings of your package on https://npmjs.com, enable trusted publishing (if you haven't already).
- Push the branch named starting with
release(see below) to GitHub.
- Create a release action. See the
-
Locally
-
On the branch with a name starting with
release(see below), just run:pnpm dlx @firefoxic/release-it # or # pnx @firefoxic/release-it
-
If the machine has no npm session, the script runs
pnpm loginbefore it touches the repository, so a login that fails leaves nothing to clean up. -
And enter OTP.
-
-
Preview
To check what a release would do — which version it picks, how
CHANGELOG.mdwould change, under which npm tag it would publish — without changing anything:pnpm dlx @firefoxic/release-it --dry-run
-
Git repository with GitHub remote
-
GitHub CLI (gh) for locally using
# First time setup — authenticate with GitHub gh auth login
The release script uses branch names to determine the release type:
release→ Stable release (e.g.,1.1.0)release-first-major→ The first major release,1.0.0(see below)release-first-major-rc→ A prerelease of it (e.g.,1.0.0-rc.1);release-first-major-→ a numbered one (e.g.,1.0.0-1)release-alpha→ Alpha prerelease (e.g.,1.0.0-alpha.1)release-beta→ Beta prerelease (e.g.,1.0.0-beta.1)release-rc→ Release candidate (e.g.,1.0.0-rc.1)release-→ Numbered prerelease (e.g.,1.0.0-1)
The script automatically determines the version bump based on changelog content:
### Changed→ Major version (breaking changes)### Added→ Minor version (new features)### Fixed→ Patch version (bug fixes)
These three are the only headings the script recognises; any other one stops the release. That narrowing is deliberate — three headings map one to one onto the three parts of a semantic version, so nothing is left to interpretation. The remaining Keep a Changelog headings fold into them:
Removed→Changed. Taking functionality away is a change, and a breaking one.Security→Fixed. Closing a vulnerability is a bug fix.Deprecated→Changed. Semantic Versioning treats a deprecation as a minor change, but deprecation messages often force a change to an established workflow, and that is a major change by nature.
While the package is below 1.0.0, ### Changed bumps the minor version, not the major one: 0.4.2 → 0.5.0. Semantic Versioning reserves 0.y.z for initial development, where anything may change at any time, so a breaking change there is expected and is not a reason to leave the range. The same cap applies to prereleases (0.5.0-beta.0, not 1.0.0-beta.0).
Reaching 1.0.0 is a deliberate decision, so it is made with a branch rather than a heading. Release from release-first-major and the version becomes 1.0.0 regardless of which headings the [Unreleased] section carries — it still has to be non-empty, since it becomes the release notes. The branch works only once: with the package already at 1.0.0 or above it refuses to run, and release takes over from there.
The first major can be tried out first: release-first-major-<name> publishes 1.0.0-<name>.0, 1.0.0-<name>.1, … under the <name> dist-tag, and release-first-major- publishes 1.0.0-0, 1.0.0-1, … under next — exactly as release-<name> and release- do for any other version. Once such a prerelease is out, release-first-major turns it into 1.0.0.
A repository whose pnpm-workspace.yaml lists packages is treated as a monorepo; nothing has to be switched on. Only pnpm workspaces are supported — the script relies on pnpm for everything else, too.
-
Every workspace package keeps its own
CHANGELOG.md, in the same format as a single package would. The root does not need one. -
Every non-private package with entries under
[Unreleased]is released; a package whose[Unreleased]section is empty is left alone. Each released package gets its own bump from its own headings, so the versions stay independent. -
The whole release is one commit — every bumped
package.jsontogether with every rewrittenCHANGELOG.md— with one tag per package, named<name>@<version>(widget@1.3.0,@acme/widget@1.3.0): a plainv1.3.0could not say which package it belongs to. The[Unreleased]link of a package changelog therefore points at such a tag:[Unreleased]: https://github.com/<user-name>/<project-name>/compare/<package-name>@0.0.1...HEAD
-
Each package is published to npm and gets a GitHub release of its own, with its changelog entries as the notes.
-
The release branch applies to all of them:
release-betamakes a beta of every package being released, andrelease-first-majortakes every one of them to1.0.0— and refuses to run if any of them is already there.
- CI/CD: Uses NPM trusted publishing
- Local: Checks the npm session up front, runs
pnpm loginwhen there is none, and only then asks for an OTP — all before the version commit, the tag and the branch exist - GitHub: Requires
gh auth loginorGITHUB_TOKENenvironment variable
-
The name of the changelog file is
CHANGELOG.md. -
The format of the changelog is consistent with Keep a changelog.
-
Descriptions of all user-important changes are already in the changelog under the heading
[Unreleased]. Ideally, you should commit them along with the changes themselves. -
If this is the first release of a package, there should be only one reference for [Unreleased] at the end of the changelog in the following format for correct reference updating:
[Unreleased]: https://github.com/<user-name>/<project-name>/compare/v0.0.1...HEAD