Automate Versioning and Changelogs with Changesets in a pnpm Monorepo
Set up Changesets so every PR carries its own version bump and changelog entry, then let GitHub Actions handle the version PR and npm publish on merge.
What you'll build
A pnpm monorepo where contributors describe their changes in a small markdown file, and a GitHub Actions workflow turns those files into correct semver bumps, per-package CHANGELOG.md updates, and npm publish runs on merge to main. No more "did we bump the right package" arguments in code review.
Prerequisites
- Node.js 18 or later
- pnpm 8+ (examples use pnpm 9 syntax, but 8.x works the same for everything here)
- An existing pnpm workspace with a
pnpm-workspace.yamland at least two publishable packages underpackages/* - A GitHub repo with Actions enabled
- An npm account with publish rights to your package scope, plus an automation access token (not a classic token tied to 2FA prompts)
If you don't have a workspace yet, the minimum pnpm-workspace.yaml looks like this:
packages:
- "packages/*"
1. Install and initialize Changesets
Install the CLI as a dev dependency at the workspace root, not inside individual packages:
pnpm add -D -w @changesets/cli
pnpm exec changeset init
Use pnpm exec (or npx changeset init) here, not pnpm changeset init. At this point there's no changeset script in package.json yet, so pnpm has nothing to run and will fail with ERR_PNPM_MISSING_SCRIPT. pnpm exec skips the scripts lookup and calls the CLI binary straight out of node_modules/.bin.
That command scaffolds a .changeset/ directory with config.json and a README.md. Commit both.
2. Configure the defaults
Open .changeset/config.json and set it up for a public npm scope publishing off main:
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
A few of these matter more than they look:
access: "public"is required if your packages are scoped (@yourorg/thing). Without it,npm publishdefaults scoped packages to private and publish will fail.updateInternalDependencies: "patch"bumps a package's own version when a workspace dependency it relies on gets bumped, even if you didn't touch that package's code. Set to"minor"if you want internal dep bumps to look bigger in your changelog.linkedgroups packages that must always share the same version number (think a core package and its official plugins). Leave it empty unless you actually need lockstep versioning, most monorepos don't.- Mark any package that shouldn't publish (internal tooling, a docs site) with
"private": truein its ownpackage.json. Changesets skips private packages automatically.
Add scripts to the root package.json:
{
"scripts": {
"build": "pnpm -r build",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "pnpm -r build && changeset publish"
}
}
Notice release runs pnpm -r build, not a root-level build. In a workspace there usually isn't a single root build step, each package builds itself. pnpm -r build runs the build script inside every workspace package that has one (pnpm handles the dependency order for you). If you'd rather keep pnpm build as the entry point in CI, define it at the root as shown above so it just delegates to -r build, but either way, changeset publish needs freshly built output on disk before it runs, so don't skip this step or publish will ship stale dist/ folders.
3. Author a changeset with every change
When you finish a PR that changes a package's public behavior, run:
pnpm changeset
This walks you through an interactive prompt: pick which packages changed, pick a bump type (patch, minor, major) for each, then write a summary. It writes a file like .changeset/tiny-lions-jump.md:
---
"@myorg/utils": minor
"@myorg/api-client": patch
---
Add retry logic to the HTTP client and export a new `sleep` helper from utils.
Commit that file alongside your code changes and include it in the PR. The summary text becomes the changelog entry, so write it for consumers, not for your reviewer.
4. Automate versioning and publishing in CI
This is where Changesets earns its keep. The official changesets/action does two things depending on repo state: if there are unreleased changeset files sitting in .changeset/, it opens (or updates) a "Version Packages" PR that bumps versions and rewrites changelogs. Once that PR is merged, the next run publishes to npm.
Create .github/workflows/release.yml:
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm -r build
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm version-packages
commit: "chore: version packages"
title: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The explicit "Build packages" step runs before the action so that both the version PR path and the publish path start from compiled output, since release also runs pnpm -r build again right before changeset publish (cheap no-op if nothing changed, and it guarantees publish never ships against a stale build if something upstream got skipped).
Note it's NODE_AUTH_TOKEN, not NPM_TOKEN, in the env block that reaches npm publish. That's because actions/setup-node writes an .npmrc referencing NODE_AUTH_TOKEN under the hood; the action needs that exact variable name to authenticate.
Add your npm automation token as a repo secret named NPM_TOKEN:
gh secret set NPM_TOKEN
(Or via GitHub UI: Settings > Secrets and variables > Actions > New repository secret.)
5. Let Actions open pull requests
By default, GitHub restricts the built-in GITHUB_TOKEN from creating pull requests. Go to Settings > Actions > General > Workflow permissions and enable:
- "Read and write permissions"
- "Allow GitHub Actions to create and approve pull requests"
Without this, the action runs but silently fails to open the "Version Packages" PR.
Verify it works
- Merge a PR to
mainthat includes a changeset file. - Check the Actions tab: the Release workflow should run and open (or update) a PR titled "chore: version packages" with a diff touching
package.jsonversions,CHANGELOG.mdfiles, and deleting the consumed.changeset/*.mdfiles. - Merge that PR.
- The workflow runs again, this time publishing. Confirm with:
npm view @myorg/utils version
It should match the version in the merged PR, and npm view @myorg/utils versions should list it in the publish history. Check the package's CHANGELOG.md in the repo for the new entry too.
Troubleshooting
- "No unreleased changesets found" and nothing happens. Expected if you merged without adding a changeset file. Run
pnpm changesetand push a follow-up commit. - Publish step fails with 403/ENEEDAUTH. Your
NPM_TOKENis either expired, scoped to the wrong org, or you used a token type requiring 2FA one-time passwords (which won't work in CI). Generate an "Automation" token from npmjs.com instead. - Version PR never appears. Almost always the workflow permissions setting from step 5. Double check both checkboxes under Actions > General.
releasescript fails with a missing script error or exits early. This usually means a package listed in the workspace doesn't have abuildscript. Either add one (even a no-op"build": "true") or scope the recursive build with a filter, e.g.pnpm --filter=\"./packages/*\" build, and confirmpnpm -r buildsucceeds locally before trusting it in CI.- Internal workspace dependency published with
workspace:*still in the range. Make sure you're runningchangeset publish(via yourreleasescript), not a rawnpm publishorpnpm publishloop. Changesets rewritesworkspace:protocol ranges to real semver ranges before publishing.
Next steps
Add a status check that fails PRs missing a changeset: pnpm changeset status --since=main in a separate CI job, or use the community changeset-bot GitHub App for automatic PR comments. If you have packages that must always version together (a core lib and its adapter packages), explore the linked config option instead of relying on updateInternalDependencies alone. The Changesets docs cover pre-release channels (changeset pre enter beta) if you need canary publishes off feature branches.
Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.
Discussion 0
No comments yet
Be the first to weigh in.