Published on

Automate npm publishing with GitHub Actions, proper changelog, and release notes

Authors
  • Jan Halama
    Name
    Jan Halama
    Title
    Developer
    Social media profiles

If you maintain at least one Node.js package, you probably know how painful the releases can be. Maybe running npm publish works for you just fine, but there are so many little details one can forget: running tests with clean dependencies, keeping the changelog up to date, creating release notes… Maintaining high quality of releases is difficult, especially in a team. Usually, it makes sense to automate the release process.

At Superface, we use GitHub Actions to automate the release workflow for npm packages. When we designed the process, we had the following requirements:

  • Start the release manually with an appropriate version bump (patch, minor, major, prepatch, preminor, premajor, or prerelease)
  • Support for manually maintained changelog file (using Keep a Changelog conventions)
  • Automatically tag the package version in the repository and publish release notes with GitHub Release
  • Publish the package to npm registry with appropriate distribution tags (latest for stable versions, next or beta for pre-release versions with)
  • Automatically publish prerelease versions on certain branches for beta testing

Since new releases cause churn for package users, we want to be certain our release notes are usable. That's why we prefer a handcrafted changelog following the Keep a Changelog convention over automatic release notes generated from commit messages.

Unlike Conventional Commits and semantic-release, we let the developer choose the release type (major, minor, patch) and the time at which the package is released.

The following steps will guide you through the setup of automated npm package releasing:

  1. Generate a new npm access token
  2. Save npm access token as a GitHub secret
  3. Add GitHub Workflow to your repository
  4. Release the npm package

Generate a new npm access token

First we need an access token for npm registry in order to publish package with GitHub Actions. Create a new access token at npmjs.com for Automation and copy it for the next step.

New access token page with Automation type selected.

Save npm access token as a GitHub secret

We will store the generated token for GitHub Actions as a repository secret. In your GitHub repository settings, visit "Secrets" → "Actions", click "New repository secret", and add the npm access token created in the previous step. We will name the secret NPMJS_ACCESS_TOKEN.

Screenshot of GitHub Secrets configuration.

Add GitHub workflow to your repository

In your project, create a new GitHub workflow file called release_package.yml, in the .github/workflows directory. Paste the following code. Optionally, you can change user.email and user.name under "Git configuration" step and uncomment the "Run tests" step along with the "Install dependencies" step.

Finally, commit the file and push the changes to your main branch.

name: Release package
on:
  workflow_dispatch:
    inputs:
      release-type:
        description: 'Release type (one of): patch, minor, major, prepatch, preminor, premajor, prerelease'
        required: true
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      # Checkout project repository
      - name: Checkout
        uses: actions/checkout@v3

      # Setup Node.js environment
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          registry-url: https://registry.npmjs.org/
          node-version: '14'

      # Install dependencies (required by Run tests step)
      #- name: Install dependencies
      #  run: yarn install

      # Tests
      #- name: Run tests
      #  run: yarn test

      # Configure Git
      - name: Git configuration
        run: |
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --global user.name "GitHub Actions"

      # Bump package version
      # Use tag latest
      - name: Bump release version
        if: startsWith(github.event.inputs.release-type, 'pre') != true
        run: |
          echo "NEW_VERSION=$(npm --no-git-tag-version version $RELEASE_TYPE)" >> $GITHUB_ENV
          echo "RELEASE_TAG=latest" >> $GITHUB_ENV
        env:
          RELEASE_TYPE: ${{ github.event.inputs.release-type }}

      # Bump package pre-release version
      # Use tag beta for pre-release versions
      - name: Bump pre-release version
        if: startsWith(github.event.inputs.release-type, 'pre')
        run: |
          echo "NEW_VERSION=$(npm --no-git-tag-version --preid=beta version $RELEASE_TYPE
          echo "RELEASE_TAG=beta" >> $GITHUB_ENV
        env:
          RELEASE_TYPE: ${{ github.event.inputs.release-type }}

      # Update changelog unreleased section with new version
      - name: Update changelog
        uses: superfaceai/release-changelog-action@v1
        with:
          path-to-changelog: CHANGELOG.md
          version: ${{ env.NEW_VERSION }}
          operation: release

      # Commit changes
      - name: Commit CHANGELOG.md and package.json changes and create tag
        run: |
          git add "package.json"
          git add "CHANGELOG.md"
          git commit -m "chore: release ${{ env.NEW_VERSION }}"
          git tag ${{ env.NEW_VERSION }}

      # Publish version to public repository
      - name: Publish
        run: yarn publish --verbose --access public --tag ${{ env.RELEASE_TAG }}
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }}

      # Push repository changes
      - name: Push changes to repository
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git push origin && git push --tags

      # Read version changelog
      - id: get-changelog
        name: Get version changelog
        uses: superfaceai/release-changelog-action@v1
        with:
          path-to-changelog: CHANGELOG.md
          version: ${{ env.NEW_VERSION }}
          operation: read

      # Update GitHub release with changelog
      - name: Update GitHub release documentation
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ env.NEW_VERSION }}
          body: ${{ steps.get-changelog.outputs.changelog }}
          prerelease: ${{ startsWith(github.event.inputs.release-type, 'pre') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Release the npm package

Update the changelog

Before you proceed to trigger a new release, add notable changes to the changelog file. Our workflow requires that you stick with the Keep a Changelog convention.

You can start with this empty CHANGELOG.md file:

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Changelog

As you commit changes to your project, write them down into the [Unreleased] section. Once you trigger a new release, unreleased changes are automatically moved under the new version heading.

Trigger new release

You can find the “Run workflow” button in your GitHub repository under “Actions” → “Release package”.

Screenshot GitHub Actions run workflow.

Our worfklow_dispatch event takes a single parameter named release type (major, minor, patch, …). The parameter is passed to the npm version command in order to bump version of the package.

Once you press the “Run workflow” button, all the magic will happen, and your npm package will be released.

Conclusion

Our workflow automates the boring parts, while keeping us in control regarding when and how to release new packages' versions. Support for the Keep a Changelog convention and GitHub Releases help us communicate changes to developers.

You can see the workflow in action in most of our package repositories, for example OneSDK for Node.js and Superface CLI.

Automate the impossible.
Superface. The LLM-powered automation agent that connects to all your systems.

Try it now