I’ve taken the time to automate the publishing process of my NPM packages, so that:
- I don’t have to remember how to publish them. All I need to do is to bump the version number and push the commit.
- The publishing envionment is always clean, consistent, and secure.
- The publishing process is transparent and easy to understand for other maintainers. I can easily delegate the publishing task to other maintainers.
- The published packages come with provenance information. This is important for security and trust.
Here’s how I did it:
Create a new environment. Let’s call it “Publish” (some people may prefer calling it “Release”, but in my opinion, in the context of GitHub, “Release” is to put a release note on GitHub’s “Releases” page, while “npm publish” is the action we are trying to automate here):
Set some protection rules:
Log into https://npmjs.com and go to the “Access Tokens” page
Generate a new Granular Access Token:
Choose a name and type in the expiration date of the token:
In the “Package and Scopes” section, choose “Read and write” permissions, and select the one (or multiple) package(s) that you would publish from the repository.
Other fields can be safely ignored. Now click “Generate token”. Copy the generated token.
Go back to the Environment settings page on GitHub. In the “Environment secrets” section, click “Add environment secret” (note it’s not environment variable, but secret!)
Name it NPM_TOKEN
and paste your newly generated token.
You might want some protections on the default branch too, if you are going to collaborate with other people and give them the write/release permission.
Go to “Settings” -> “Branches” -> “Add branch ruleset”.
Type in a name; make it active.
Target the default branch:
The default rules suffice for me. They prevent any user from deleting history from the default branch or forcing pushing to it.
Create a .github/workflows/publish.yml
file in the project.
Typically, it may look like this, but may vary depending on the project:
name: Publish
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
release:
# Use Publish environment for deployment protection
environment: Publish
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install
# Note: `--no-git-checks` is required due to https://github.com/pnpm/pnpm/issues/5894
- run: pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Add "provenance": true
to the package.json
file under the publishConfig
field.
Optionally, create a .github/MAINTENANCE.md
file to explain the release process to other maintainers.
I use the following template for the maintenance document:
This document explains how to perform the project's maintenance tasks.
### Creating a new release
Anyone with write access to the repository can request a new release. To do so, follow these steps:
1. Run `pnpm version <patch|minor|major>` locally to bump the version number and create a new commit / tag.
2. Push the commit and tag to the repository by running `git push --follow-tags`.
3. The release will be automatically published to npm by GitHub Actions once approved by an administrator.
4. Go to <https://github.com/<OWNER>/<PROJECT>/releases/new> and create a new release with the tag that was just created. Describe the notable changes in the release notes.
I initially wanted to bump the version directly from the GitHub Actions. However, I later found no ideal way to sign the commit from GitHub Actions, especially when there’s a large commit object (such as in create-vue
where I need to generate a large set of template snapshots). So I gave up and settled with the manual version bumping.