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.

Marcio Barrios
ITNEXT

--

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:

  1. Validate the changes (running tests, checking that changes satisfy ESLint or Prettier, or checking maximum size of a package)
  2. If all the previous checks were successful, then a static build of any Styleguide could be build or even deployed
  3. After that CHANGELOG.md and package version are updated and a new commit is generated
  4. The next step is to push to Github both the tags and a release version (for this step is needed a Github auth token)
  5. 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 an npm 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 as npm run release:tags && npm run release:github)
  • npm ci is similar to npm 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 of git commit to be able to easily create a commit message with your changes
  • npm run release to trigger an automatic version change, thanks to npm scripts before running the release will be triggered prerelease script (validations, styleguide static build and automatic github contributors), and after the release script will be triggered postrelease script (it will push CHANGELOG.md and package.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 an npm publish is performed (before that prePublishOnly 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.

--

--