· monorepo / pnpm / turborepo

How to Set Up Changesets for Automated Monorepo Releases

Set up @changesets/cli v2.31.0 in a pnpm + Turborepo monorepo — from your first changeset to a fully automated GitHub Actions publish pipeline.

By

2,017 words · 11 min read

If you’re manually bumping versions across packages and updating changelogs by hand, Changesets will buy back that time within the first week. This tutorial wires up @changesets/cli v2.31.0 in a pnpm + Turborepo 2.x monorepo, from initial install through a fully automated publish pipeline on GitHub Actions.

Who this is for

Mid-level devs with a working pnpm + Turborepo monorepo who are bumping versions manually today. If you haven’t set up pnpm workspaces yet, see How to Set Up a pnpm + Turborepo Monorepo from Scratch first.

What Changesets is (and isn’t)

Changesets is a release-management tool for monorepos. It manages three things: which packages need version bumps, what those bumps should be (patch/minor/major), and what goes in the changelog.

What it’s not: a build tool, a publish orchestrator, or a CI system. It writes files. You decide when to consume them.

The lifecycle is three commands:

changeset add       → developer declares intent (bump type + summary)
changeset version   → changesets apply to package.json + CHANGELOG.md
changeset publish   → npm publish for every package with a new version

That’s it. Everything else is configuration around those three steps.

developer PR


changeset add
    │  writes .changeset/<random-name>.md

merge to main


changeset version
    │  bumps package.json, writes CHANGELOG.md
    │  removes .changeset/*.md files

pnpm install      ← required: update lockfile before publish


changeset publish
    │  npm publish for each bumped package

git tag + push

Prerequisites

  • Node 20+
  • pnpm 9+ (pnpm 10 recommended)
  • Turborepo 2.x
  • An npm account with publish access for your scoped packages

Install and init

Install at the workspace root:

pnpm add -Dw @changesets/cli

The -w flag installs to the workspace root. Do not install per-package.

Initialize:

pnpm changeset init

This creates .changeset/config.json and .changeset/README.md. Commit both.

Configure .changeset/config.json

The default config works for most setups, but three fields require a decision:

{
  "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}
FieldDefaultWhat it does
access"restricted"npm publish access for scoped packages
commitfalsewhether changeset version auto-commits
fixed[]packages that must version-bump together
linked[]packages that share a version if any one bumps
baseBranch"main"branch Changesets diffs against

The access gotcha: the default is "restricted", which means npm will reject your publish for any scoped package (@org/pkg) unless you pay for npm private packages. Set it to "public" if your scoped packages are public:

{
  "access": "public"
}

fixed vs linked: both group packages together, but they behave differently.

fixed forces every listed package to bump to the same version, even if only one of them has a changeset:

{
  "fixed": [["@myorg/core", "@myorg/react", "@myorg/vue"]]
}

linked only syncs packages that already have a changeset — if @myorg/react has a patch changeset and @myorg/vue has none, linked bumps react and leaves vue alone. fixed bumps both.

Use fixed when your packages are always shipped as a matched set (design system tokens + components). Use linked when packages share a version range but can release independently.

Adding a changeset

When you’re ready to open a PR that touches a package, add a changeset:

pnpm changeset

The interactive prompt asks two things: which packages this change affects, and what kind of bump (patch / minor / major). Then it asks for a summary — one line, written for the changelog reader, not for yourself.

The command writes a file to .changeset/ that looks like this:

---
"@myorg/core": minor
"@myorg/utils": patch
---

Add `createToken()` utility and expose `BaseComponent` as a named export from core.

Commit this file with your PR. Reviewers can see exactly what release impact the PR carries — no “bump version” commit required.

Versioning on merge

When PRs merge to main, apply the accumulated changesets:

pnpm changeset version

This reads every .changeset/*.md file, applies the version bumps to each package.json, writes the changelog entries, and deletes the changeset files.

pnpm-specific step: after changeset version, run pnpm install before publishing. The package.json files changed, so the lockfile is now out of sync. Publishing without this step will either warn or fail depending on your pnpm version.

pnpm changeset version
pnpm install            # ← required: update pnpm-lock.yaml
pnpm changeset publish

Publishing

pnpm changeset publish

This runs npm publish for every package whose version in package.json does not exist on the registry. It also creates git tags per package.

You need NPM_TOKEN in your environment. If the token is missing, publish fails with a 401 that shows up as a generic auth error — not always obvious. Set it before you run:

export NPM_TOKEN=your-token-here
pnpm changeset publish

After publishing, push the tags:

git push --follow-tags

If you’re publishing a standalone package outside a monorepo, see How to publish an npm package the right way in 2026 for the single-package workflow.

GitHub Actions automation

The official Changesets action handles the versioning → PR → publish loop automatically. Here’s a working workflow for pnpm:

name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build packages
        run: pnpm run build

      - name: Create Release PR or publish
        uses: changesets/action@v1
        with:
          publish: pnpm changeset publish
          version: pnpm changeset version && pnpm install --no-frozen-lockfile
          commit: 'chore: version packages'
          title: 'chore: version packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

The action works in two modes depending on what’s on main:

  1. Pending changesets: it opens (or updates) a “Version Packages” PR. That PR shows the version diffs and changelog entries. When you merge it, the action runs again in mode 2.
  2. No pending changesets: it publishes whatever packages have new versions.

Note pnpm install --no-frozen-lockfile in the version input. The regular pnpm install --frozen-lockfile will fail here because changeset version just changed package.json files, making the lockfile stale. The --no-frozen-lockfile flag lets pnpm regenerate it for the version PR.

Advanced: private packages and access control

Packages you don’t want to publish can be excluded from Changesets entirely:

{
  "ignore": ["@myorg/internal-app", "@myorg/test-utils"]
}

Ignored packages never get changesets applied to them. They can still show up as dependents — if core bumps, internal-app won’t bump, but its package.json reference to core will update.

For packages that exist only as workspace internals and should never be published, set "private": true in their package.json. Changesets respects this flag and skips them at publish time without needing the ignore array.

Advanced: snapshot releases

Snapshot releases let you publish a pre-release version from any branch — useful for testing a PR in a downstream project before merging.

pnpm changeset version --snapshot alpha
pnpm changeset publish --tag alpha --no-git-tag

The published package version will be something like 1.2.0-alpha-20260529120000. The --no-git-tag flag prevents snapshot tags from cluttering your tag history.

Your consumers can then test with:

pnpm add @myorg/core@alpha

Snapshot releases don’t require changesets to be present — if there are no changeset files, the snapshot version is computed from the latest stable version with a timestamp suffix.

Advanced: pre-release mode

Pre-release mode is for managing a beta or RC branch over time. Enter it on your pre-release branch:

pnpm changeset pre enter beta

From this point, changeset version produces versions like 1.0.0-beta.0, 1.0.0-beta.1, etc. Changesets accumulate across commits on this branch.

When you’re ready to go stable:

pnpm changeset pre exit
pnpm changeset version   # produces the final stable version

Pre-release mode is tracked in .changeset/pre.json. Commit that file — it tells Changesets which mode the branch is in and which packages have pre-release bumps pending.

Turborepo integration

Don’t name your publish script publish. npm and pnpm treat publish as a reserved lifecycle hook and can run it at unexpected times. Use publish-packages instead:

{
  "scripts": {
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "publish-packages": "turbo run build lint test && changeset version && changeset publish"
  }
}

Add the Changesets commands to your turbo.json. Remember: the Turborepo 2.x config key is tasks, not pipeline:

{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

changeset version and changeset publish don’t need Turborepo tasks — they operate at the workspace level, not per-package. Running turbo run build lint test before them ensures every package is built and passes before any version is bumped or any publish goes out.

Changesets vs the alternatives

Changesetssemantic-releaserelease-itLerna
Monorepo-firstYesVia pluginVia pluginYes
Manual changelog entriesYes (changeset files)No (auto from commits)BothBoth
pnpm workspacesFirst-classVia pluginVia pluginLimited
Multi-package coordinationfixed / linked groupsPer-package configPer-package config--since flag
Pre-release supportBuilt-in (pre enter)Built-inBuilt-inBuilt-in
Config complexityLowMediumMediumHigh
Actively maintainedYesYesYesYes (by Nx team)

Changesets is the right default for a pnpm monorepo. Its changeset-file model scales well to multi-team repos where PR authors declare intent rather than relying on commit message conventions.

semantic-release and release-it shine when you want zero developer overhead — automatic version detection from conventional commits, no PR-time decision required. The tradeoff is that everyone on the team has to follow commit conventions consistently.

Lerna has been actively maintained by the Nx team since 2022. It’s a reasonable choice if you’re already in the Nx ecosystem, but it adds overhead when you’re not.

Common pitfalls

access: "restricted" for scoped packages

This is the most common publish failure. The default config has "access": "restricted", which is correct for private npm packages but wrong for public ones. If you see a 402 or 403 from the registry when publishing a @scope/package, this is why. Set "access": "public" in config.json.

Missing pnpm install between changeset version and changeset publish

changeset version rewrites package.json files. The lockfile is now stale. If you publish without running pnpm install, you’ll either get a warning or a hard failure depending on your frozen-lockfile setting. Always run pnpm install after versioning.

pnpm catalog + Changesets (verify before publishing)

pnpm catalogs (catalog: protocol in pnpm-workspace.yaml) are not yet natively supported by Changesets. PR #1714 (--enable-pnpm-catalog) was open and unmerged as of May 2026. If you’re using catalogs, check whether this PR has landed before publishing. The workaround: add a manual patch changeset for any package that uses catalog: dependencies when those catalog entries change.

Naming the script publish

If your root package.json has "publish": "...", pnpm will run it as a lifecycle hook when any package publishes, not as your intended top-level script. Use publish-packages or any other name.

commit: true in Turborepo setups

commit: true in config.json makes changeset version automatically commit the version bump. This can conflict with Turborepo CI pipelines that expect the version commit to come from a PR merge. Leave it at false (the default) and let the Changesets GitHub Action or your CI handle the commit.

References