Automate npm publishing with GitHub Actions, proper changelog, and release notes
Tags: node, npm, github, productivity
Published: 2022-06-22 08:55:43

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](https://docs.npmjs.com/cli/v8/commands/npm-version) (`patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, or `prerelease`)
- Support for manually maintained changelog file (using [Keep a Changelog][keepachangelog] conventions)
- Automatically tag the package version in the repository and publish release notes with [GitHub Release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository)
- Publish the package to npm registry with appropriate [distribution tags](https://docs.npmjs.com/cli/v8/commands/npm-dist-tag) (`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][keepachangelog] convention over automatic release notes generated from commit messages.

Unlike [Conventional Commits](https://www.conventionalcommits.org/) and [semantic-release](https://github.com/semantic-release/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](#generate-a-new-npm-access-token)
2. [Save npm access token as a GitHub secret](#save-npm-access-token-as-a-github-secret)
3. [Add GitHub Workflow to your repository](#add-github-workflow-to-your-repository)
4. [Release the npm package](#release-the-npm-package)
   - [Update the changelog](#update-the-changelog)
   - [Trigger new release](#trigger-new-release)

## 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](https://docs.npmjs.com/creating-and-viewing-access-tokens) at npmjs.com for `Automation` and copy it for the next step.

![New access token page with Automation type selected.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vaxt49ugjm7nllqcb6al.png)

## Save npm access token as a GitHub secret

We will store the generated token for GitHub Actions as a [repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository). 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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kyfbzicr5m0yhhdptv0n.png)

## 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.

```yaml
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@v2.3.4

      # Setup Node.js environment
      - name: Setup Node.js
        uses: actions/setup-node@v2
        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][keepachangelog] convention.

You can start with this empty `CHANGELOG.md` file:

```markdown
# 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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pfjg8xgsqaekbq8xi47t.png)

Our [worfklow_dispatch](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) event takes a single parameter named `release type` (`major`, `minor`, `patch`, …). The parameter is passed to the [npm version command](https://docs.npmjs.com/cli/v8/commands/npm-version) 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](https://github.com/superfaceai/one-sdk-js) and [Superface CLI](https://github.com/superfaceai/cli).

We are curious about how you manage your releases.
Is your process fully automated, or do you just run `npm publish` locally? Do you maintain a changelog manually or generate it from commit messages? Share your approach in the comments! 👇

[semver]: https://semver.org/
[keepachangelog]: https://keepachangelog.com