Publish to NPM automatically with GitHub Actions

Published 8/14/2020

This article is part of a series:


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

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

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 "[email protected]"
    # 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.