A workflow guide for npm package authors

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.

Assumptions

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 1.2.3.

OK let’s get into the four workflow stages:

  1. Issues
  2. Branches
  3. Pull requests
  4. Releases

1. Issues

Semver side note

For example, the next version of my-package (currently version 1.2.3) could be:

  • 1.2.4 if it contains a fix (‘patch’)
  • 1.3.0 if it contains a new feature (‘minor’)
  • 2.0.0 if 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

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:

This issue is in the milestone “v4.0.1”

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.

2. Branches

Main branch

Checks

"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.

Branching strategies

  • 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.

Single-issue release

npm version patch

(Or 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.

Multi-issue release

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

(Or minor or patch).

That would give my-package the version 2.0.0.

If you do plan to publish a pre-release version to npm, run:

npm version premajor

(Or preminor or prepatch.)

Which would result in the version 2.0.0-0.

Note that if you were to sort by version number, the order is 1.2.3, 2.0.0–0, 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 2.0.0-beta.0.

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 2.0.0-0 means.

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 2.0.0–0 to 2.0.0-1.

npm version takes the same arguments as semver --increment, so if you want to try before you buy, you can always play with the semver package directly, like:

npx semver 1.2.3 -i premajor

This prints 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

Creating

When you create a PR, link it to the issue, either via a magic keyword in the PR description like fixes #44 or using linked issues.

Merging

Never merge any code changes to main if the PR doesn’t have a change to the version in package.json.

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.

Post-merge

You should set GitHub to automatically delete branches on merge.

4. Releases

Pre-releases

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 npm install flow.
  • 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:

npm view

Under the dist-tags section, you should see something like:

  • latest: 1.2.3
  • next: 2.0.0-0

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 2.0.0-0 to 2.0.0.

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.

Real releases

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 package.json again).

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 latest tag.

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 my-package@2.0.0 latest

Now npm i my-package (which is the same as npm i my-package@latest) will install 2.0.0.

You can now delete the next dist-tag:

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 main or default or 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 …

npm publish

Check to make sure the world is as it should be:

npm view

Post-publish checklist:

  • 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 2.0.1.

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.

Release Notes

GitHub releases

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.

Subscribing to releases for a GitHub repo

CHANGELOG.md

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.

I like web stuff.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store