Releasing an npm package is like trekking through the wilderness. The ideal path will depend on how experienced you are, how much of a hurry you’re in, and whether you’re hiking alone or in a group. This post describes one way to plot a course (the best way).
Truth be told I’m writing this primarily for Future David, because I know he’ll forget the npm commands and make the same mistakes over and over again if I don’t write it all out for him. But maybe you’ll find it useful too.
I’m assuming you know what npm is and know how to publish a package. If I’ve assumed incorrectly, please do read npm’s Getting Started guide then come back so that I can be correct. You may also want to read npm’s Packages and Modules docs if I under-explain something.
It’s assumed that you’re using GitHub to track issues/PRs.
This post will revolve around a package called
my-package that is currently on version
OK let’s get into the four workflow stages:
- Pull requests
Every piece of work on the codebase should be described in a GitHub issue. Stick to a pattern of one issue, one branch, one pull request. The aim is that years from now you can find your way from any single line of code, via git blame, to a commit, a PR, and an issue explaining the rationale behind that code.
Semver side note
Any work you do on a package is going to result in a new version, and at any point in time, a package’s next release could have one of three version numbers, according to the rules of semantic versioning.
For example, the next version of
my-package (currently version
1.2.3) could be:
1.2.4if it contains a fix (‘patch’)
1.3.0if it contains a new feature (‘minor’)
2.0.0if it contains a breaking change (‘major’)
The usual semver rules do not apply when the first number of the version is
0. This is confusing and not widely understood so you should not publish to npm with a version below
1.0.0 (npm agrees).
Some changes to the repository don’t require a version change, e.g. fixing a typo in the readme or tweaking tests. Just keep in mind npm won’t update the readme on npmjs.com until you publish a new version.
Back to GitHub issues…
If working on an issue is likely to result in a new release, it should be labelled with ‘semver-major’, ‘semver-minor’, or ‘semver-patch’ when first assessed. A guess is fine, this is only to assist in selecting which issues to work on later.
Managing multi-issue releases
I’ve experimented with a ‘semver kanban’ project board in GitHub, with columns for the upcoming versions.
This works nicely in a busy repo with a high issue-to-release ratio, but is probably overkill for more modest projects.
GitHub milestones are another way to group issues into versions and when you name them the same as your versions, they serve as a handy bit of metadata for an issue:
Of course you don’t want to create a milestone for every single issue if you have a low issue-to-release ratio, so use your judgement.
master) branch of your repo should always represent the latest version of the package as it exists on npm. This not only simplifies things for you and your fellow contributors, but is beneficial for users of your package looking at your source code on GitHub. This is especially true if your readme file is your documentation; you don’t want to confuse your users by having the github.com readme not match the npmjs.com readme.
You should have some checks in place that run before you do important things. E.g. in the
scripts section of your
package.json you might have something like this:
"checks": "npm run build && npm run lint && npm run test",
"prepublishOnly": "npm run checks",
"preversion": "npm run checks"
Any script starting with
pre will run before the thing that comes after the
pre. So this ensures that you can’t publish your package or even bump the version if it’s broken.
You can improve on this with various CI/CD solutions to catch problems sooner, but the above will at least prevent you from shipping demonstrably dodgy code to users.
Note: to have a script run before
publish, you don’t use
prepublish, the definition of which is “Does not run during npm publish”. Yes, yes that is dumb.
Each release will take one of two forms:
- Single-issue release. You will work on an issue and publish the change to npm as a new version.
- Multi-issue release. You will batch together work from several issues and publish them as a single new version.
In both cases, you’ll do the work in ‘feature branches’ (one per issue). For single-issue releases you’ll bump the version in each feature branch. For multi-issue releases, you’ll create an ‘integration branch’ to hold all the changes, and bump the version in that branch.
Let’s look at each in a bit more detail.
Create a branch, do the work. Run:
npm version patch
npm version minor or
npm version major). This will increment the version in
package.json and create a commit with the appropriate tag. Push your changes (with tags) to GitHub.
Move on to the ‘pull requests’ step.
Create a GitHub milestone (if you’ve decided to use milestones) for the upcoming release and add all the issues you want to bundle into this next version. If it’s a major version, all issues with the label
semver-major should be included, or have a good reason to be excluded.
Create an integration branch for the release, e.g.
release-2.0.0. You must absolutely not call the branch
v2.0.0 — later on, npm is going to create a tag with that name, and having a branch and a tag with the same name is going to make you cry out of holes you didn’t even know you had. So I’ve heard, from a friend, who is not me.
You’ll set the version number in this integration branch, but not actually do any work here.
If you don’t plan to publish a pre-release version to npm (more on this in “4. Releases”), run:
npm version major
That would give
my-package the version
If you do plan to publish a pre-release version to npm, run:
npm version premajor
Which would result in the version
Note that if you were to sort by version number, the order is
2.0.0. This means that if you’d already bumped the version to
2.0.0, because you thought you wouldn’t do a pre-release, and now want to do a pre-release with the version
2.0.0-0, you’d actually need to move the current version backwards. You can do this manually but I’d try and avoid it. If you’re not sure whether you’ll want a pre-release or not, then save this versioning step until all the work is done.
You can add a text flag as well for clarity. E.g. this:
npm version premajor --preid=beta
…would result in
On one hand, that’s clearer to the user that this is not a normal version, on the other, it’s one more argument to remember and type. Which you chose might be swayed by what percentage of people consuming this version are your users, vs internal folk that know what
If you’ve already released a pre-release and want to increment the pre-release part of the version only, run:
npm version prerelease
This would increment
npx semver 1.2.3 -i premajor
2.0.0–0 to your console and makes no change at all to your codebase. (BTW, IMO, the best documentation for learning exactly what all the version commands do is the comments in the source code of the semver package.)
Push the integration branch (with tags).
If you plan on using GitHub releases (more on this below), create draft release notes targeting the
v2.0.0 tag that the
npm version command created. If you’ll instead use a
CHANGELOG.md, start a new section in there for this version.
You will now work on each of the issues that you want to make part of this release, each one branching from this integration branch, and targeting it when the pull request is created.
The individual feature branches don’t need to do anything with the version number.
When the last issue is done and merged into the integration branch, you will merge the integration branch into
main and publish it.
3. Pull requests
If you want to get feedback before creating an actual pull request, create a draft pull request. I reckon draft pull requests are great, they provide 80% of the benefit of “pair programming” with 20% of the time investment.
If you’re going to publish a pre-release to npm, you may do that before merging the branch, otherwise…
Never merge any code changes to
main if the PR doesn’t have a change to the version in
If you want a linear commit history, merge PRs with the squash and merge option with a commit message summarising the change. Having said that, if you’re not screwing up, you shouldn’t need to go sifting through your commit history, so do whatever works for you. Don’t rebase as that destroys commits before merging which adds pointless complexity to the process.
It’s a good idea to merge PRs, release to npm, and update release notes all at the same time, so don’t merge unless you’ve also carved out the time to do the rest of the release-related tasks.
If you made changes to the readme that might affect links, post-merge is a good time to test these (you can’t really test this 100% before merging to
(You’ll generally do the below in a feature or integration branch, not in
You might want to publish a pre-release version for a few different reasons, e.g.:
- You’ve changed the way the package installs, and want to test a full
- You’d like other people to be able to install a beta version to help you test.
- You’re nervous.
There are two conceptual parts to a ‘pre-release’ package:
- The package has a version number with a pre-release suffix (described earlier)
- The package is published to npm with a tag other than ‘latest’ (described very soon)
To publish to npm with a tag other than ‘latest’ (the default tag), run the following (are you paying attention? Don’t get this wrong.)
npm publish --tag=next
Check that all is well:
dist-tags section, you should see something like:
If you got that wrong, you may now proceed to have a panic attack.
If you got it right, you may proceed to try out your new version locally (e.g. in a project that uses your package) with
npm i my-package@next
When you’re done testing, update the branch to remove the pre-release component from the semver version. E.g.
npm version major
This would bump
Note: creating and publishing versions from git branches (other than
main) should be minimised, but merging broken code should be minimised too. Use your judgement, find a balance.
Path a: the cautious release
Even if you’re not publishing a pre-release version, you can still dip a toe in the water using tags. For example, to publish
2.0.0 to the
next tag …
npm publish --tag=next
You can then ask a few select people to test with
npm i my-package@2 or
npm i my-package@next.
Assuming everything works fine, these users won’t need to perform any other steps once you do the real release (unlike releases with a pre-release suffix in the version, in those cases users would need to update their
At this point,
2.0.0 exists on npm, but typing
npm i my-package would still install
1.2.3, because that version has the
If you have a demo site (e.g. a codesandbox), go update that now to use the new version (if you don’t, maybe you should? I mean, maybe not, but have you considered it?)
When you’re really ready to go, you don’t need to publish again, just point the
latest dist-tag to the new version:
npm dist-tag add firstname.lastname@example.org latest
npm i my-package (which is the same as
npm i my-package@latest) will install
You can now delete the
npm dist-tag rm my-package next
The whole ‘dist-tag’ concept and execution is quite nicely done. Well done, npm folks. (Although
latest should probably be called
current, and it’s weird that
dist-tag is the only command where you have to specify the package name, but good enough.)
If you’re only 50% nervous about doing a release,
--dry-run is there to hold your hand, it “does everything publish would do except actually publishing to the registry. Reports the details of what would have been published.”
Path b: the confident release
Hey, you know what you’re doing, let’s just release this puppy. Pull
main, deep breath …
Check to make sure the world is as it should be:
- If you haven’t already, update any demo sites using the package.
- Publish the release notes.
- Close the milestone, if there was one.
- Tell the world. If it’s a popular package and you’ve fixed something people have publicly noted (e.g. on Stack Overflow), go and share the good news.
- Test one last time, find bug, have panic attack, get to work on
If you’ve just fixed a fatal flaw, you may want to deter people from using the version you’ve replaced. You can do this by deprecating that version. See the thoroughly titled npm page Deprecating and undeprecating packages or package versions for more info.
The benefit of GitHub releases is that you can create drafts, releases are structured (e.g. links to tags and commits, date stamps, attached assets), users can subscribe to them (I do this for all repos I currently use), and this is arguably where developers go looking for your release notes.
The benefit to a
CHANGELOG.md file is that it’s portable, searchable (since it’s one file, not stingy little pages like GitHub releases), and this is arguably where developers go looking for your release notes.
Pick whichever works for you, or if you’d rather just do what I tell you to do, use GitHub releases.
There are plenty of tools that will create one from the other and even create both by looking at your commit history, so don’t worry about being “locked in” to any particular approach.
Some folks like to try and automate this process. If this works for you, great, whatever. But in my opinion, writing release notes is a nice little break from writing code, a time to summarise the things you’ve been toiling away on, and to let your users know why you’ve changed things and what’s in it for them.
Speaking of automation, some folks like to try and automate the whole release process. I would suggest you don’t do this without first thinking about the return on investment. If you can’t quantify how many hours you spend on automatable release tasks, and how many hours you expect to spend researching, setting up and maintaining the automation system, then you’re jumping the gun. (Relevant XKCD.)
That’s all, folks. Thanks for reading.