Now that we are all done with CI, let's tackle CD.
There are a couple of ways we can set up publishing to NPM. For example, we could publish when pushing to a certain branch, creating a tag, creating a commit in a specific pattern, or by creating a release on GitHub directly.
For my project flooent, I went with the last option.
Here's what I want:
My project is currently on version 2. The code for this (the latest version) is on the branch "latest". The code for version 1 is on the branch "v1-latest". The reason for these branch names will become apparent later. I want to be able to publish, of course, the current version, but also version 1 in case a patch is needed.
And here's what I need the pipeline to do:
- When publishing a release, trigger a workflow
- In the build, first, check out the branch the release is targetting
- Update the version in package.json based on the release tag
- Build the project and run tests to make sure everything works
- Publish the project to an NPM registry
- Push the version change to GitHub
The .yml file
If you haven't already, create the directories .github/workflows
in the root of your repository. Inside, create a file "cd.yml".
To get the release info we will be using data passed in by GitHub which is available under the variable "github".
To see everything it contains, you can echo it out in your workflow:
- run: echo "${{ toJson(${{ github.event) }}"
And here is the content of the cd.yml
file. I will not go through it line by line, instead, I added comments to each line explaining what is happening.
name: NPM publish CD workflow
on:
release:
# This specifies that the build will be triggered when we publish a release
types: [published]
jobs:
build:
# Run on latest version of ubuntu
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
# "ref" specifies the branch to check out.
# "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted
ref: ${{ github.event.release.target_commitish }}
# install Node.js
- name: Use Node.js 12
uses: actions/setup-node@v1
with:
node-version: 12
# Specifies the registry, this field is required!
registry-url: https://registry.npmjs.org/
# clean install of your projects' deps. We use "npm ci" to avoid package lock changes
- run: npm ci
# set up git since we will later push to the repo
- run: git config --global user.name "GitHub CD bot"
- run: git config --global user.email "github-cd-bot@example.com"
# upgrade npm version in package.json to the tag used in the release.
- run: npm version ${{ github.event.release.tag_name }}
# build the project
- run: npm run build
# run tests just in case
- run: npm test
# publish to NPM -> there is one caveat, continue reading for the fix
- run: npm publish
env:
# Use a token to publish to NPM. See below for how to set it up
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# push the version changes to GitHub
- run: git push
env:
# The secret is passed automatically. Nothing to configure.
github-token: ${{ secrets.GITHUB_TOKEN }}
NPM auth token
We publish to NPM using a token NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
. But we still have to configure the "NPM_TOKEN" secret.
First, go to npm, in the settings go to "Auth Tokens", and click the button "Create New Token".
Copy the token and head over to the project settings of your GitHub repository. Go to "Secrets" and click "New Secret". Give it the name "NPM_TOKEN" and for the value, paste the token inside.
With the secret set up, GitHub will now be able to resolve ${{ secrets.NPM_TOKEN }}
with the token from your GitHub secrets.
Fixing an NPM caveat
It already works at this point!
We can create a release with the tag 2.1.0 and target the "latest" branch. We can also create a release with the tag "1.3.4" targetting the "v1-latest" branch.
Make sure, when you create the release, to use a tag following semver. E.g. 2.0.0, v2.0.0, 2.0.0-beta.1, etc.
However, there is one caveat when publishing an old version.
You see, when you do npm install flooent
it will actually do npm install flooent@latest
behind the scenes. And when you do npm publish
it will actually do npm publish --tag latest
.
"latest" is a reserved tag for NPM. However, even though the project is already on version 2, publishing a fix for version 1 will make it the "latest" version. This means when somebody installs the package afterward, he will get version 1 instead of version 2. That's of course not what we want.
So to fix this, we have to specify a different tag when publishing. One way to do this is by adding a default publish tag to the v1 branch package.json:
"publishConfig": {
"tag": "v1-latest"
}
But there's also another way. Remember the branch names I chose? "latest" and "v1-latest". Sounds just like tags we can use for NPM.
So instead of fiddling with package.json in each branch, all we have to do is go to our yaml file and replace
- run: npm publish
with
- run: npm publish --tag ${{ github.event.release.target_commitish }}
The reason I chose v1-latest
over just v1
is that npm tags must not follow semver. Otherwise, NPM would not be able to distinguish a tag from a specific published version.
Setting up the different parts for CD was admittedly the hardest part to get right, but the resulting yaml file is actually quite small and straight forward.
Top comments (3)
Wow, good job on the detailed post!
Just so you know, there is a NPM repo that does most of this deployment process by itself.
I'd love to get some of your feedback and points to improve.
Oh awesome! For my case I also needed it to update the version in package.json and publish to specific NPM tags.
Do you think it's something relevant to add?