An image of Hugh Rawlinson's face

Avoiding pitfalls when installing semantic-release for npm library packages

I recently set up semantic-release on an open source repo. I was sold on the idea of unsentimental commits - the idea that it should be easy and quick to deploy changes to packages, and that you shouldn't worry about batching changes together to reduce the number of versions that get published. In the past, I've found myself not deploying breaking changes that I already have written just so that I can batch them with hypothetical breaking changes that I would like to make. That tended to mean that features and fixes end up delayed, which isn't in anyone's best interests. Unsentimental commits take that power out of my hands - releases are automated, and happen whenever commits are merged.

This post covers some pitfalls that I encountered in setting up my semantic-release tooling, that I want to help you avoid, so I wrote this blog post. Let's go!

This post won't cover setup instructions, so if you haven't yet installed semantic-release and given your CI system of choice an npm token, please follow the instructions from the semantic-release team.

Avoid merging commits with non-standard messages!

If you merge commits to master that don't follow the commit standard that semantic-release is expecting, semantic-release won't publish your package. If your commit contains a breaking change, but isn't following the standard, semantic-release won't publish your package. This means that you'll have an undeployed breaking change on your master branch, and if you subsequently release a minor or patch change, you could end up deploying the breaking change to a patch version and breaking your consumer's code.

You can avoid merging commits that don't follow the commit standard by installing commitlint, which will fail your PR builds when they contain invalid commit messages. You can also install husky to have your commits linted as you commit, before you even push to your git remote.

Commitlint setup instructions are available, as are husky setup instructions.

You should run commitlint in your Continuous Integration tool when a pull request is built, so that you can't merge Pull Requests that contain commits with non-standard messages. Check out commitlint's CI Setup documentation for instructions on how to set up commitlint in your CI. Since my project uses the Travis CI service, I was able to use the @commitlint/travis-cli package - but you'll be able to run commitlint on Pull Request review builds on any CI provider.

If you have already merged a commit containing a non-standard commit message, don't worry, you can fix it by pushing an empty commit with a standard compliant commit message summarizing the changes that you've already merged. For example, if you have merged a breaking change to master, you might publish an empty commit with the following message.

 feat(node version): drop 8, add 13 and 14

This commit represents lots of commits to master that have not triggered a release, including lots
of dependency version bumps, adding support for node 13 and 14, and dropping support for node 8,
which is a breaking change. Therefore, this commit will trigger a major version bump and release.
This commit itself is empty as it represents previous commits and serves as a commit to be version

BREAKING CHANGE: Drop support for node 8

semantic-release will pick up that commit and publish a new version. Just make sure that if you have pushed breaking changes to master that your empty commit marks a breaking change, or else you'll publish a breaking change to the same major version, which could cause bugs in projects that consume your package. But if you have released a breaking change with a minor or patch version bump, don't worry, we'll cover that below!

Avoid GitHub invalidating commit messages in squash-merges

When I made my empty commit that would have published a new major version of my package, I opened a pull request so that the CI pipeline would run and confirm that I hadn't broken any of my code, or pushed an invalid commit message (since commitlint runs on CI). The review build passed, and so I merged the pull request – but then the master build failed to publish because the commit message failed to lint! What happened?

When you merge a pull request on GitHub and choose the "Squash and Merge" option, GitHub will squash all the commits in the pull request into one and automatically name the new pull request. It will append the pull request number, something like (#403) to the commit message title. The @commitlint/config-conventional package that implements the Conventional Commits commit format standard for the commitlint tool enforces a maximum line length of 100 on the commit header. GitHub's pull request number had pushed my commit message past the limit, causing it to fail on master after succeeding on the branch.

You could resolve this by avoiding squash merging, making sure to keep your commit messages very short, or manually checking each PR to see how much breathing room you have.

None of those options sound particularly appealing to me. They all depend on a person remembering what to do*, rather than a tool to automatically do the right thing and free up the developer's mind. If this comes up again, I'm considering writing a quick GitHub Check to warn me that I need to either reword a commit or not squash it.

* Yes, you can disable squash merging per repository on GitHub - but you have to remember to do that on each repository.

Make sure Dependabot writes commit messages properly

I use Dependabot to automatically open pull requests on my repos whenever my dependencies publish new versions, so that I can make sure I'm always up to date on any bugfixes or security fixes. Dependabot writes commit messages that fit the standard if that's what it sees you have been doing, but if you're installing semantic release in an existing repository where you haven't always been using conventional commits, it might default to non-standard commit messages.

You can explicitly configure Dependabot to explicitly to follow conventional commits using the commit_message property of it's configuration, which is documented on the dependabot website.

If you have PRs that dependabot opened before you configured it to use conventional commits that don't have correctly formatted commits, I haven't found a way to get it to recreate them with the correct message, so you may have to upgrade those dependencies manually.

Make sure your CI won't be blocked from deploying to the registry

The semantic-release setup guide explains how to create an npm deploy key so that you're able to publish your package to the public registry. If you have two factor authentication set up on your npm account, which you really should, the npm CLI may prompt semantic-release for an OTP on your CI, which it won't have access to. To make sure semantic-release is able to publish, you can set your npm 2FA to auth-only mode. This will make npm only ask for an OTP when you're logging in, and not when you publish a package.

I'm not extremely comfortable with this, since if someone finds a way to get a copy of your deploy key, they'll be able to publish versions of your package without any additional information. To minimize this risk, make sure your CI provider is configured only to expose your NPM_TOKEN on master builds, and be vigilant in making sure that you don't merge a PR that steals your NPM_TOKEN.

If you know of a better way, please let me know!

Do your best to make sure your contributors don't struggle with commit messages

One of my reservations about installing semantic-release in my repository was that it would degrade the developer experience for any existing or prospective contributors. If a developer who is unfamiliar with conventional commits were to get bothered by a linter about the commit message when they're used to writing messages a particular way, how likely are they to give up?

A combination of commitizen and husky will give the developer feedback as they commit on their machine, so that they can get it right before they push to their fork of my repo.

Be ready to jump in on PRs to help contributors get it right!

Some things I would consider, but haven't done yet:

  • Wrap commitizen and commitlint in scripts so that I can customize and improve upon the error messages
  • Write a GitHub bot to automatically suggest standard compliant commit messages on open PRs that fail commitlint
  • Information in the GitHub Pull Request Template explaining the commit message format required, and letting the contributor know that we're grateful for their commit regardless of whether the message lints, and that we're happy to write a new message ourselves when we review their code.

Don't worry if you make a mistake!

I made a lot of mistakes when I was setting Meyda up with semantic-release, commitlint, and husky. All of the above issues are inspired by mistakes I made! Luckily, I was able to recover from all of them as described above. So don't worry!

If you've published a breaking change without bumping the major version number, that may cause breaks in the consumers of your library. Don't worry though - you can push a new commit containing a BREAKING CHANGE: section, triggering semantic-release to publish a new major version of your library, and then mark your erroneously published bad version as deprecated on the registry. The npm command to deprecate a package version is documented on the npm website.

Hope those tips help with your semantic-release setup! There are quite a lot of packages involved in this setup, here's a list:

If you found this blog post useful, please let me know on Twitter!