How to automate versioning and publication of an npm package
Guide, tips and tricks to work with npm scripts and external packages to automate version management and package release process.
TL;DR You can see our approach to automate the release process in the last section of the article.
At Xing Spain we started a new project some months ago, a React component library that several products will use, so the natural way to share this library is through an npm package.
In order to share an npm package it’s important to release versions in a consistent way, and we knew from the beginning that we wanted to automate this process as much as possible.
This post describes how we are automating the process of versioning and publication. If you want the simplest possible way I would recommend semantic-release, it’s a fully automated version management and package publishing tool.
In our case the main reason to not use semantic-release was because we didn’t want to release a new version for every push done in master, and we wanted complete control of every step. Another reason to not use semantic-release could be that you need to tweak too much your CI setup. In any case both approaches are based on the same foundation of structured commit messages.
In this article I’m assuming you already know how to publish an npm package but you want to improve all the manual process involved. If you are not familiar with the publication process, you can start reading the documentation.
Workflow highlights
The only requirement to be able to automate versioning and publication of a package is that every commit should follow a message convention, we follow the Conventional Commits Specification.
So our commits look like this:
6f88099 - (tag: v1.8.0, master) chore(release): 1.8.0
333dce9 - feat: add composable Avatar component
ffb2263 - fix: add correct styles to Label component
7d34334 - chore: update Jest to 24.7.1
You can check different examples of Conventional Commits in their documentation site.
Enforce conventional commits
It’s easy to forget about the commit convention so to be consistent we use commitzen to generate our commits and husky to manage a Git commit-msg hook to validate the commit message.
You can install husky and commitzen easily:
npm i -D husky @commitlint/{config-conventional,cli}
And this setup needs to be added to your package.json
:
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
To test it just try to commit a change with a message that doesn’t follow the commit convention and it will throw an error.
In our case we are also using a pre-commit hook triggering lint-staged to lint and format the changes before commiting them, in the last section you can see an example from our package.json
.
Update package version based on commits
We use standard-version to automatically change the version based on the commit history.
To install standard-version just run:
npm i -D standard-version
And then you can create the release script in your package.json
:
{
"scripts": {
"release": "standard-version"
}
}
Now you could run npm run release
to trigger a version update.
Take into account that standard-version will change your version number following these guides:
- A
git commit -m “fix: …”
commit will trigger a patch update (1.0.0 → 1.0.1) - A
git commit -m “feat: …”
commit will trigger a minor update (1.0.0 → 1.1.0) - A
BREAKING CHANGE: …
in the commit body and with any type of commit will trigger a major update (1.0.0 → 2.0.0)
As a summary, standard-version will do these tasks:
- Bumps the version in
package.json
- Updates
CHANGELOG.md
- Commits both files
- Tags a new release
What standard-version doesn’t do is pushing the commit to your remote repository, so after runningnpm run release
you need to perform git push --follow-tags origin master
and publish the package.
Some cool extras you can do in the release process
There are a several extra tasks that can be done automatically during the release process, for example:
- A Github Release using conventional-github-releaser (creates a nicer release in Github listing the new features, fixes or breaking changes)
- Automatically update contributors in
package.json
using git-authors-cli - Generate a static version of a styleguide (could be Storybook, Styleguidist, Docz…)
- Check that your package doesn’t reach a predefined maximum size, using size-limit
You can see an example on how to use some of these extras in the last section of this article.
Trigger a release
Once a commit reaches master, to perform a release just needs one command: npm run release
What happens next? A lot of things could happen behind the curtain, all the magic is done using some npm packages and npm scripts.
This could be a normal workflow using standard-version, but the possibilities are endless:
- Validate the changes (running tests, checking that changes satisfy ESLint or Prettier, or checking maximum size of a package)
- If all the previous checks were successful, then a static build of any Styleguide could be build or even deployed
- After that
CHANGELOG.md
and package version are updated and a new commit is generated - The next step is to push to Github both the tags and a release version (for this step is needed a Github auth token)
- The last step is to publish the new version to npm, this is usually done in a CI server but can be done locally as well
Keep reading if you wanna see the packages and the setup we use to perform a similar workflow.
A real example of automatic versioning and release
If you want to apply a similar approach first you need to install some dependencies:
npm i -D standard-version husky lint-staged @commitlint/{config-conventional,cli} commitizen conventional-github-releaser git-authors-cli conventional-github-releaser npm-run-all eslint prettier
And then you can update your package.json
with some scripts and configs:
"scripts": {
"commit": "git-cz",
"test": "...",
"build": "...",
"lint": "eslint src/**",
"styleguide:build": "...",
"prettier:check": "prettier --check 'src/**/*.{js,mdx}'",
"validate": "run-s test lint prettier:check",
"prerelease": "git checkout master && git pull origin master && npm i && run-s validate styleguide:build && git-authors-cli && git add .",
"release": "standard-version -a",
"postrelease": "run-s release:*",
"release:tags": "git push --follow-tags origin master",
"release:github": "conventional-github-releaser -p angular",
"ci:validate": "rm -rf node_modules && npm ci && npm run validate",
"prepublishOnly": "npm run ci:validate && npm run build",
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.mdx": [
"prettier --write",
"git add"
],
"src/**/*.js": [
"prettier --write",
"eslint --fix",
"git add"
]
}
This is taken directly from our package.json
, but I removed some scripts that are not related to the automation process and also emptied some others that will be set depending on your needs.
To understand properly the example you need to know some npm scripts features and tricks:
- There are several npm scripts that will be automatically triggered, for example before or after a package is installed, or before a package is published, check them in the npm documentation, they are really helpful
- Also any custom script can be executed running
npm run <script_name>
and pre and post commands with matching names will work (e.g. prevalidate, validate, postvalidate) prePublishOnly
is triggered before the package is prepared and packed, only for annpm publish
(so this is a perfect place to validate your package and perform the build)- Using npm-run-all package you can run several scripts in parallel or sequential (so
npm run-s release:*
is the same asnpm run release:tags && npm run release:github
) npm ci
is similar tonpm install
but tweaked to be used in automated environments, you can read more about npm-ci in the npm documentation
After this brief explanation about npm scripts, let me explain our real workflow for any developer:
npm run commit
instead ofgit commit
to be able to easily create a commit message with your changesnpm run release
to trigger an automatic version change, thanks to npm scripts before running the release will be triggeredprerelease
script (validations, styleguide static build and automatic github contributors), and after the release script will be triggeredpostrelease
script (it will pushCHANGELOG.md
andpackage.json
changes to the remote repository and and perform a new release in Github)- In our case, the last step is done in our CI environment (Jenkins), we parse every commit pushed to master and if it’s a release commit (e.g.
chore(release): 1.8.0
) then annpm publish
is performed (before thatprePublishOnly
will be automatically triggered to validate and build the code)
To be honest in a Continuos Integration environment we shouldn’t trigger the release locally. We want to manually trigger every release but a better place could be from a Jenkins jobs for example.
And that’s all, I hope some of our workflow is helpful for your project or at least to understand all the automation possibilities that you can achieve with some npm magic. And this process could be even more sophisticated sending a release notification to a Slack or publish a new tweet.
In any case as I recommend you at the beginning of this article, for most projects probably it makes more sense to use semantic-release and automate the whole process easily.