JS Library 5/7: Continuous Deployment

JS Library 5/7: Continuous Deployment

 · 8 min read

Note: Check out the entire supplemental source code for this blog series.

Continuous Deployment

In this tutorial, we’ll use the popular library semantic-release to automate releases to npm and github as well as automatically generating release notes, tagging release commits, deprecating old versions of our release, and publishing notifications to Slack.

semantic-release

semantic-release has various plugins that each have different functionality. In this tutorial, we’ll install semantic-release and six different plugins.

Let’s start by running npm install semantic-release --save-dev.

The default options would serve most teams well, but it defaults to managing several branches and running several plugins. We’ll start blank and add things one at a time so we can get familiar with all the different pieces. First, we’ll create a .releaserc.js configuration file. We’ll specify that we want semantic release to manage the main branch and we won’t specify any plugins yet:

./.releaserc.js

module.exports = {
  branches: ['main'],
  plugins: []
};

Plugin 1 - commit-analyzer

The first plugin we’ll add is the @semantic-release/commit-analyzer plugin. This plugin will scan your commits since your last release and determine if the next release should be major, minor, or patch release in accordance with semantic versioning.

The following is a brief description of SemVer posted on semver.org:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards compatible manner, and
  3. PATCH version when you make backwards compatible bug fixes.

Following SemVer and our previous commit conventions, we’ll specify that a chore should not cause a version bump, a fix should cause a patch version bump, a feat should cause a minor version bump, and any commit that has the words “BREAKING CHANGE” in it should cause a major version bump.

This is what our updated configuration looks like now:

./.releaserc.js

...
  plugins: [
    ["@semantic-release/commit-analyzer", {
      "releaseRules": [
        { "breaking": true, "release": "major" },
        { "type": "feat", "release": "minor" },
        { "type": "fix", "release": "patch" },
        { "type": "chore", "release": false }
      ],
      "parserOpts": {
        "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
      }
    }]
...

Plugin 2 - release-notes-generator

The second plugin we’ll configure is @semantic-release/release-notes-generator. This plugin will generate release notes. There are several options that we could configure, but for this plugin we’ll just leave the default options:

./.releaserc.js

  plugins: [
    ...
    "@semantic-release/release-notes-generator"
  ]

Plugin 3 - github

Next, we’ll install the @semantic-release/github plugin. This will publish a GitHub release and comment on linked pull requests and issues. There are several configuration options for this plugin as well, but we’ll leave the defaults:

./.releaserc.js

  plugins: [
    ...
    "@semantic-release/github"
  ]

Plugin 4 - npm

We’ll setup the @semantic-release/npm plugin next and leave the defaults as well. This plugin will publish our library to npm:

./.releaserc.js

  plugins: [
    ...
    "@semantic-release/npm"
  ]

Plugin 5 - npm-deprecate-old-versions

The @semantic-release-npm-deprecate-old-versions plugin can automatically deprecate old versions of our packages. This is not a built-in plugin so we’ll need to install it with npm install semantic-release-npm-deprecate-old-versions --save-dev.

We’ll configure it to only support the latest version for each of the two latest major versions and to deprecate the rest:

./.releaserc.js

  plugins: [
    ...
    ["semantic-release-npm-deprecate-old-versions", {
      "rules": [
        {
          "rule": "supportLatest",
          "options": {
            "numberOfMajorReleases": 2,
            "numberOfMinorReleases": 1,
            "numberOfPatchReleases": 1
          }
        }
        "deprecateAll"
      ]
    }]
  ]

Plugin 6 - slack-bot

Lastly, we’ll install the semantic-release-slack-bot plugin. This plugin will post to Slack whenever a new version is released. This plugin is also not built-in so we’ll need to install it with npm install semantic-release-slack-bot --save-dev.

Here is the configuration:

./.releaserc.js

  plugins: [
    ...
    [
      'semantic-release-slack-bot',
      {
        notifyOnSuccess: true,
        notifyOnFail: true
      }
    ]
  ]

Release workflow

Now we should have semantic-release fully configured. Our .release.js should look like this:

./.releaserc.js

module.exports = {
  branches: ['main'],
  plugins: [
    [
      '@semantic-release/commit-analyzer',
      {
        releaseRules: [
          { breaking: true, release: 'major' },
          { type: 'feat', release: 'minor' },
          { type: 'fix', release: 'patch' },
          { type: 'chore', release: false }
        ],
        parserOpts: {
          noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES']
        }
      }
    ],
    '@semantic-release/release-notes-generator',
    '@semantic-release/github',
    '@semantic-release/npm',
    [
      'semantic-release-npm-deprecate-old-versions',
      {
        rules: [
          {
            rule: 'supportLatest',
            options: {
              numberOfMajorReleases: 2,
              numberOfMinorReleases: 1,
              numberOfPatchReleases: 1
            }
          },
          'deprecateAll'
        ]
      }
    ],
    [
      'semantic-release-slack-bot',
      {
        notifyOnSuccess: true,
        notifyOnFail: true
      }
    ]
  ]
};

Now we’ll create a new Release workflow. First we’ll extract out a build action that can be used by both our Build and Release workflows:

./.github/actions/build/action.yml

name: Build
runs:
  using: "composite"
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 'lts/*'
    - name: Install dependencies
      run: npm ci
      shell: bash
    - name: Lint
      run: npm run lint
      shell: bash
    - name: Test
      run: npm run test
      shell: bash

Our Build workflow will now have just one step:

./.github/workflows/build.yml

name: Build
on:
  pull_request:
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Build
        uses: ./.github/actions/build

And our Release workflow will have two steps:

./.github/workflows/release.yml

name: Release
on:
  push:
    branches:
      - 'main'
jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Build
        uses: ./.github/actions/build
      - name: Release
        env:
          GITHUB_TOKEN: $
          NPM_TOKEN: $
          SLACK_WEBHOOK: $
        run: npx semantic-release

It’s important that we set our Release workflow to only run on the main branch.

You’ll notice that there are three environment variables listed under the env key in the Release workflow:

  • GITHUB_TOKEN: This is automatically provided as a GitHub secret when using GitHub actions, so this will just work.
  • NPM_TOKEN: For this we will need to create an npm automation token and add it as a GitHub secret in our repository settings.
  • SLACK_WEBHOOK: For this we will need to create a slack app and activate incoming webhooks for a particular channel. This will give you a url in the form of https://hooks.slack.com/services/XXX/YYY/ZZZ. This is the url that you must provide as the SLACK_WEBHOOK GitHub secret.

Summary

Now, when we open a pull request our CI system will automatically ensure the code builds and lints as well as all tests pass. Whenever we merge a pull request, our library will automatically be published and our release notes will automatically be generated.

Next, we can look at how to maintain different versions of the same library using maintenance branches.