Automated release on GitHub Actions

7 min read

There are many tools available to automate the release process for npm packages, such as lerna-lite for monorepos, Release It! for single packages etc. They can be used to automatically bump the version and generate changelog based on the commit messages using Conventional Commits, create a GitHub release, create a git tag, and publish the package to npm etc.

Generally, the release process is triggered manually by running a command on local machine. But automating the release process on GitHub Actions can be more convenient.

This guide documents how to configure GitHub Actions to automatically release npm packages on every commit using release-it. However, these steps can be adapted to other similar tools as well.

Step 1

Install release-it and @release-it/conventional-changelog as dev dependencies:

yarn add --dev release-it @release-it/conventional-changelog

Configure release-it in the package.json file:

package.json
{
  ...
  "release-it": {
    "git": {
      "commitMessage": "chore: release ${version}",
      "tagName": "v${version}"
    },
    "npm": {
      "publish": true,
      "skipChecks": true
    },
    "github": {
      "release": true
    },
    "plugins": {
      "@release-it/conventional-changelog": {
        "preset": {
          "name": "conventionalcommits"
        },
        "infile": "CHANGELOG.md"
      }
    }
  },
  ...
}

The npm.skipChecks option is set to true to skip authentication checks done by release-it, since it will be handled by GitHub Actions using "Trusted Publisher". Not setting this option will lead to authentication error when running the release workflow.

Step 2

Setup your GitHub workflow as a "Trusted Publisher" on npm. You need to do it for each package at https://www.npmjs.com/package/[package-name]/access (replace [package-name] with your package name):

This allows you to publish packages using OpenID Connect (OIDC) authentication without needing an npm token.

Legacy setup with npm token

It is recommended to use the trusted publisher feature if possible instead of npm tokens for better security. In addition, npm tokens are only valid for 90 days, making it quite inconvenient for automated workflows.

To use npm tokens for authentication instead, create a npm token with publish access. You can create one at https://www.npmjs.com/settings/[username]/tokens (replace [username] with your username):

  • Click on "Generate New Token" and select "Granular Access Token"
  • Provide a token name and expiration date
  • Under "Packages and scopes", select "Read and write" for permissions
  • Then select "Only select packages and scopes" and select the package you want to publish
  • Click "Generate token" and copy the token

Then the token needs to be added as a secret in the GitHub repository:

  • Go to the repository and click on "Settings"
  • Click on "Secrets and variables" and choose "Actions"
  • Click "New repository secret" and add the token as npm_PUBLISH_TOKEN
  • Click on "Add secret" to save the token

This token will be used to authenticate with npm to publish the package.

Warning

Other collaborators on the repo can push actions that use this token and update the npm package acting as the user associated with the token. Make sure to use this only if you trust the collaborators on the repository.

Step 3

Create a GitHub personal access token. You can create one at github.com/settings/personal-access-tokens/new:

Legacy setup with classic token

Alternatively, you can create a classic token with the repo scope under Developer settings in your profile settings. However, it is highly recommended to use granular access tokens with the least required permissions for better security.

Then the token needs to be added as a secret in the GitHub repository:

A personal access token is necessary to be able to push the changes back to the repository if the release branch is protected. The user associated with the token needs to have admin access to the repository and be able to bypass branch protection rules.

Warning

Other collaborators on the repo can push actions that use this token and push commits acting as the user associated with the token. Make sure to use this only if you trust the collaborators on the repository.

If there are no branch protection rules in the repository, then the GITHUB_TOKEN secret (available by default) can be used instead of a personal access token. Note that commits made by using GITHUB_TOKEN won't trigger other workflows.

Step 4

Create a GitHub Actions workflow file in .github/workflows/release.yml with the following contents:

.github/workflows/release.yml
name: Release package
on:
  workflow_run:
    branches:
      - main
    workflows:
      # List of workflows that runs tests, linting, etc.
      # This ensures that the release is only triggered when the tests pass.
      - CI
    types:
      - completed
 
jobs:
  check-commit:
    runs-on: ubuntu-latest
    # Skip if the workflow run for tests, linting etc. is not successful
    # Without this, the release will be triggered after the previous workflow run even if it failed.
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    outputs:
      skip: ${{ steps.commit-message.outputs.skip }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
 
      # Check if the commit message is a release commit
      # Without this, there will be an infinite loop of releases
      - name: Get commit message
        id: commit-message
        run: |
          MESSAGE=$(git log --format=%B -n 1 $(git log -1 --pretty=format:"%h"))
 
          if [[ $MESSAGE == "chore: release "* ]]; then
            echo "skip=true" >> $GITHUB_OUTPUT
          fi
 
  release:
    runs-on: ubuntu-latest
    needs: check-commit
    permissions:
      contents: read
      id-token: write
    # Skip if the commit message is a release commit
    if: ${{ needs.check-commit.outputs.skip != 'true' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          # This is needed to generate the changelog from commit messages
          fetch-depth: 0
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
 
      - name: Setup Node.js
        uses: actions/setup-node@v3
 
      - name: Install dependencies
        run: yarn install --immutable
        shell: bash
 
      - name: Configure Git
        run: |
          git config user.name "${GITHUB_ACTOR}"
          git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
 
      - name: Update npm
        run: npm install -g npm@latest
 
      - name: Create release
        run: |
          yarn release-it --ci
        env:
          GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

There are 2 important things to note in this workflow:

The "Update npm" step is included to ensure that the latest version of npm is used, which is necessary for the trusted publisher feature.

Legacy setup with npm token

If you are using npm token instead of setting up a trusted publisher, then replace the Create release step with the following:

.github/workflows/release.yml
- name: Create release
  run: |
    npm config set //registry.npmjs.org/:_authToken $npm_PUBLISH_TOKEN
    yarn release-it --ci
  env:
    GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
    npm_PUBLISH_TOKEN: ${{ secrets.npm_PUBLISH_TOKEN }}
    npm_CONFIG_PROVENANCE: true

Setting npm_CONFIG_PROVENANCE to true will generate a provenance statement when publishing the package. This lets others verify where and how your package was built. This also needs the id-token: write permission in the permissions section of the job.

You can also remove the "Update npm" step since we only need it to use the trusted publisher feature.

After configuring, this workflow automatically publishes a new version of the package on every commit to the main branch after the CI workflow is successful.

Release workflow

Instead of publishing on every commit, an alternative way could be to have the release workflow configured, and run the workflow manually from the Actions tab in the repository when a new release is needed. This can be done by using the workflow_dispatch event to the on section:

.github/workflows/release.yml
name: Release package
on:
  workflow_dispatch:
 
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      # Same steps as before

See the GitHub documentation for Manually running a workflow for more details.