Skip to content
Dev Tools Intermediate Tutorial

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.

Mariana Souza
Mariana Souza
Senior Editor · Jul 11, 2026 · 9 min read
Automate Versioning and Changelogs with Changesets in a pnpm Monorepo

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.yaml and at least two publishable packages under packages/*
  • 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 publish defaults 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.
  • linked groups 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": true in its own package.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

  1. Merge a PR to main that includes a changeset file.
  2. Check the Actions tab: the Release workflow should run and open (or update) a PR titled "chore: version packages" with a diff touching package.json versions, CHANGELOG.md files, and deleting the consumed .changeset/*.md files.
  3. Merge that PR.
  4. 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 changeset and push a follow-up commit.
  • Publish step fails with 403/ENEEDAUTH. Your NPM_TOKEN is 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.
  • release script fails with a missing script error or exits early. This usually means a package listed in the workspace doesn't have a build script. Either add one (even a no-op "build": "true") or scope the recursive build with a filter, e.g. pnpm --filter=\"./packages/*\" build, and confirm pnpm -r build succeeds locally before trusting it in CI.
  • Internal workspace dependency published with workspace:* still in the range. Make sure you're running changeset publish (via your release script), not a raw npm publish or pnpm publish loop. Changesets rewrites workspace: 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 Souza
Written by
Mariana Souza · Senior Editor

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

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading