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)