Setting up automated version release should be easy. Luckily for NPM it almost is. Here is how I setup all my NPM modules to stop thinking about details of the publishing process.
- Use a good generator to start
- Setup semantic release
- Adjust semantic release
- Final thoughts
- Update 1: project generator
- Update 2: pre-release from branches
- Related
Use a good generator to start
I finally listened to the voice of reason by Kent C. Dodds in An Argument for Automation. It does make sense to invest time and automate tasks that one does repetitively. In my case, I have created more than 300 Node packages. Every new package requires writing the same boilerplate text, installing the usual packages, etc.
I wanted something more than npm init --yes
and wrote a Yeoman
generator generator-node-bahmutov that asks only 3 questions,
yet provides at least 10 features for more robust modules: git hooks,
linting and unit tests, security checks, package size info, etc.
My workflow currently involves very fews steps (let us create a module named "foo")
1 | create new GitHub repo 'foo' |
The generated package.json
has ALL fields NPM and module users need:
license, author, bugs, home page.
Setup semantic release
If you have not seen semantic-release demo videos, please go and watch it now. This awesome tool allows the CI to publish new version of your module to NPM (and create beautiful, readable Git tag) only after unit tests pass. The setup is very simple. Install the tool once and then call it in your folder.
1 | npm install --global semantic-release-cli |
Answer a couple of questions and either copy the tokens to your CI, or allow the setup to create a Travis build (my preferred choice).
The best feature: the new version will only be published if there are
meaningful changes. The semantic-release
inspects your git log since the
last release to find if there are commit messages that follow
semantic format. Here is an example of my commits
1 | git commit -m "feat(log): add logging feature" |
The above 3 commits follow the convention and can be parsed automatically. When the CI sees the above 3 messages, it will decide what the version bump should be. I have a module largest-semantic-change that can give you the bump based on the messages. The largest change wins; in this case the version should increment the "minor" number because the first commit added a new feature.
Adjust semantic release
The default semantic release settings should be enough for 90% of open source libraries that use GitHub with Travis CI. But there are a few changes I sometimes make to better suit my needs.
Non Travis CI
If you use something other than TravisCI, you need to grab the GitHub and
NPM tokens created during semantic-release-cli setup
step and set them
as environment variables on your CI.
You also need to tell semantic-release
step that it is OK to release
from a different CI environment. For example if you use CircleCI you
can use my condition-circle
module as a plugin.
1 | npm install --save-dev condition-circle |
Then tell the semantic-release
to use the module as a plugin using
package.json
1 | "release": { |
It is simple to write your own environment check as a plugin, see the Travis verification plugin code.
Use specific Node version to publish
NPM registry used to be finicky when publishing from some Node / NPM versions, thus it was useful when testing across multiple versions to only publish from a specific one. Module condition-node-version restricts semantic release to run on a matching Node version prefix.
1 | npm install --save-dev condition-node-version |
Again, it is configured using release
object in package.json
1 | "release": { |
We can combine multiple verification plugins to run in sequence like this
1 | "release": { |
The above configuration will allow publishing from CircleCI running Node v4.3.x.
Simple commit format
I usually like the Git commit message convention, but one thing bothers me about it. See if you notice the inconsistency.
I fixed something fix(something): now it works
I added a new feature without breaking stuff feat(feature): something new
I rewrote something, breaking existing API
1 | feat(something): adjust something |
Notice how we have to use multi-line comment to describe a breaking change?
I hate this, and always forget the "magic word" - is it "BREAKING", "BREAK",
"BREAKING CHANGE" or "BREAKING CODE"? Do I need a colon after "BREAKING"?
Why the breaking change is using the message feat(...): ...
with "feat" type?
So instead I follow my own simple-commit-message
format. It is very consistent: you just need to remember
semver version format major.minor.patch
1 | major(scope): breaking change in API |
There are also a few aliases for completeness.
1 | "break(scope): ..." is synonym to "major(scope): ..." |
If you want to commit without release you can use chore
, WIP
or skip
pre-git
commit message validation using git commit -n
option.
1 | chore(tests): writing more tests |
The best thing about simple-commit-message
- it can be
used as a plugin
to semantic-release
to determine the new version.
1 | { |
This makes it simple to commit local code, yet control how the new version is published.
1 | // current latest version 1.0.0 |
It has never been easier to keep pumping out features, yet allowing the users to make an intelligent decision about upgrading.
Never break dependent modules
By semantic version rules, a minor
and patch
releases should be backwards
compatible. Yet, the NPM (and every other artifact repository) is full of
modules where a fix release breaks every other module.
Relying on humans to write a lot of tests and be very diligent
about commit messages is not a foolproof strategy.
We can do better. We can test a new version of our module against
dependent projects in the wild before publishing it. There is a stand alone
tool dont-break, but there is also a semantic-release
plugin
dont-crack.
1 | npm i -D dont-crack |
Determine which modules are dependent on the current module, hopefully
these modules have good tests that exercise our dependency.
Add the GitHub urls to the release
configuration. For example
for our made up module "foo", we found two modules "foo-client" and "foo-user".
1 | { |
When the semantic-release
runs on CI and determines that there is a new
minor
or patch
version to be published, it first tests these projects
with new version of foo installed. Only the these tests pass against the
new code, the new version is allowed to be published.
This preserves the semantic backwards compatibility when releasing small
updates. It is ok to break the tests for dependent projects, but only
if you release a major
version increment.
Final thoughts
With the above approach I never think about versions. I just think about each code change and its commit message. The rest is fully automated.
Update 1: project generator
I have extended my generator to setup semantic release
with a couple of useful plugins. Just run yo node-bahmutov:release
to get them!
Update 2: pre-release from branches
Since I use semantic-release I set up pre-release whenever I want to publish a beta version of a package. These releases are configured from branches in the package.json
For example, the next configuration published the latest
tag from the branch master
, and also published new pre-releases from the branch prepare-v2
.
1 | { |
So if our official latest published NPM version is 1.9.1
with the tag latest
, then the new breaking feature would be published as 2.0.0-prepare-v2.1
with the tag prepare-v2
that you can try.
I have written a lot about semantic release in these blog posts
- Automatically publish to NPM
- How to setup semantic release on Circle CI
- Manual semantic release setup
- Semantic release on GitLab
I also recommend checking out semantic-release-gitlab if you use GitLab for OSS or for private modules. If you want to bump version using simple message convention according to semantic rules, take a look at stand alone next-ver tool. If you want to publish module from CI, but only for manually upgraded versions, check out ci-publish.