Github Actions Workflow to Publish NPM Packages From A Monorepo

In a scenario where a team of developers are working on the same codebase it is desirable to centralise code deployments via some sort of Continuous Integration (CI) pipeline. Indeed, restricting the ability of individual developers to deploy code directly may well be a security compliance requirement in your organisation. The same applies to publishing NPM packages: rather than running npm publish from the individual developer's machine, it is preferable to use CI for this, which ensures that code published is the same as what is in the repo, avoids version collisions and provides an audit trail.

While it is possible to create a fully automated CI workflow to publish packages whenever changes are pushed to the repository, in this guide I opt for a manually triggered workflow for simplicity (a chronically underappreciated quality) and because I typically make several commits before I am ready to publish a new package version.

This guide describes a Github Actions workflow for publishing NPM packages stored in a monorepo. Working with packages in a monorepo provides a few additional challenges that are fortunately easily overcome.

The following folder structure is assumed for the purposes of the workflow below:

.github/
  workflows/
    publish.yaml
packages/
  foo/
  bar/

This is the complete workflow in ./github/workflows/publish.yaml file:

name: 'Publish Package'

on:
  workflow_dispatch:
    inputs:
      package:
        type: choice
        description: Package to Publish
        options:
          - foo
          - bar
      version:
        type: choice
        description: Version Increment
        options:
          - patch
          - minor
          - major
          - prepatch
          - preminor
          - premajor
          - prerelease

jobs:
  publish:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./packages/${{ github.event.inputs.package }}
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: |
          git config --global user.name "${GITHUB_ACTOR}"
          git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
      - id: version
        run: echo "version=$(npm version --git-tag-version=false ${{ github.event.inputs.version }})" >> $GITHUB_OUTPUT
      - run: git add package.json && git commit -m "publish ${{ github.event.inputs.package }} ${{ steps.version.outputs.version }}"
      - run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: git push

The inputs section of the configuration defines the package names and version strings accepted by the npm publish command. These are also used for populating the dropdown values when manually triggering the workflow. (Screenshot)

The working-directory option makes sure the npm commands will run in the relevant monorepo package directory.

By default npm version will create a git commit and tag with the version. Because we are in a monorepo with several packages where a bare version tag is ambiguous, the version is instead saved as an output variable and used in a commit message along with the package name.

Because I typically scope my packages, I need to add --access public to the npm publish command. If you are paying for the ability to host private packages then you can remove that option.

Finally, the package registry access token needs to be configured as secret either on the repo or GitHub org level. (Screenshot)