Publishing packages with npm and CircleCI
Oct 27 2019 ยท Software Engineering
A common workflow
In recent years, I’ve pushed more and more for common, automated, deployment processes. In practice, this has usually meant:
- Code is managed with Git, and tags are used for releases
- Tags (and hence releases) are created via GitHub
- Creating a tag executes everything in the CI pipeline + a few more tasks for the deployments
The result is that all deployments go through the same process (no deploy scripts run on personal machines), in the same environment (the CI container). It eliminates discrepancies in how things are deployed, avoids workflow differences and failures due to environment variance, and flattens the learning curve (developers only need to learn about Git tags).
Here I’ll present how I’ve been approaching this when it comes to publishing npm packages, with deployment tasks handled via CircleCI. The flow will look something like this:
Setting up CircleCI
First things first, we need the CircleCI pipeline to trigger when a tag is created. At the bottom of your circle.yml file, add filter for “deployment.”
version: 2
jobs:
build:
docker:
- image: circleci/node:10.0.0
working_directory: ~/repo
steps:
- checkout
#
# Other stuff (run npm install, execute tests, etc.)
# ...
deployment:
trigger_tag:
tag: /.*/
Authenticating with the npm registry
Create an npm token and expose it as an environment variable in CircleCI (in this case, I’ve named it NPM_TOKEN). Then, add a step to authenticate with the npm registry in your circle.yml:
version: 2
jobs:
build:
docker:
- image: circleci/node:10.0.0
working_directory: ~/repo
steps:
- checkout
#
# Other stuff (run npm install, execute tests, etc.)
# ...
- run:
name: Authenticate with registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
deployment:
trigger_tag:
tag: /.*/
Versioning
Things get a little weird when it comes to versioning. npm expects a version declared in the project’s package.json file. However, this goes against managing releases (and thus versioning) with Git tags. I see two potential solutions here:
- Manage versions with both Git and npm, with the npm package version mirroring the tag. This would mean updating the version in package.json first, then creating the Git tag.
- Only update/set the version in package.json within the pipeline, and set it to the version indicated by the Git tag.
I like the latter solution, as forgetting to update the version number in package.json is an annoyance that pops up frequently for me. Also, dealing with version numbers in 2 places, across 2 systems, is an unnecessary bit of complexity and cognitive load. There is one oddity however, you still need a version number in package.json when developing and using the npm tool, as npm requires it and will complain if it’s not there or in an invalid format. I tend to set it to “0.0.0”, indicating a development version; e.g.
{
"name": "paper-plane",
"version": "0.0.0",
// ...
}
In the pipeline, we’ll reference the CIRCLE_TAG environment variable to get the Git tag and use to correctly set the version in package.json. Based on semantic versioning conventions, we expect the tag to have the format “vX.Y.Z”, so we’ll need to strip away the “v” and then we’ll use “X.Y.Z” for the version in package.json. We can use npm version to set the version number:
npm --no-git-tag-version version ${CIRCLE_TAG:1}
Note the –no-git-tag-version flag. This is necessary as the default behavior of npm version is to commit the tag to the git repo.
Publishing
Publishing is simply done via npm publish. Pulling together the CIRCLE_TAG check, applying the version, and publishing into a deploy step, we get something like this:
version: 2
jobs:
build:
docker:
- image: circleci/node:10.0.0
working_directory: ~/repo
steps:
- checkout
#
# Other stuff (run npm install, execute tests, etc.)
# ...
- run:
name: Authenticate with registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- deploy:
name: Updating version num and publishing
command: |
if [[ "${CIRCLE_TAG}" =~ v[0-9]+(\.[0-9]+)* ]]; then
npm --no-git-tag-version version ${CIRCLE_TAG:1}
npm publish
fi
deployment:
trigger_tag:
tag: /.*/
… and we’re done 🚀!
For further reference, this circle.yml uses the steps presented above.