How to Automate Version Bumping, CHANGELOG, and GitHub Actions Deployments in a pnpm Monorepo with Changesets
Anyone who has managed a monorepo has run into this at least once. You fix package A, forget to bump the version of package B that depends on it, or you're writing the CHANGELOG by hand and suddenly wonder "when did this even change?" I remember opening four package.json files at once and editing versions one by one when I first took over a monorepo. Looking back, it was a pretty nerve-wracking way to work.
Changesets solves this problem by separating the intent to change from the act of releasing. Developers record the impact of a change—which packages are affected and to what degree—in a small markdown file, and the tool handles the rest: version calculation and CHANGELOG generation. This article assumes readers who are already running or have set up a pnpm workspace-based monorepo. If you're new to GitHub Actions or have no experience publishing to npm, some sections may require additional reference material.
After reading this article, you will be able to install Changesets in a pnpm workspace and set up the entire pipeline yourself—from version bumping to npm publishing—automated with GitHub Actions.
Core Concepts
Let's start with the concepts behind how Changesets actually solves this problem.
How Changesets Thinks About Version Management
Most version management tools parse commit messages to determine versions. They look at prefixes like feat: or fix: to decide whether something is a minor or patch change. This works fine, but in a monorepo it gets ambiguous when a single commit touches multiple packages at different levels of change.
Changesets takes a different approach: change details are explicitly written in separate files.
---
"@myorg/ui": minor
"@myorg/utils": patch
---
Added `variant` prop to Button component, fixed bug in utility functionAs these files accumulate in the .changeset/ directory, the changeset version command reads them, bumps each package's version according to SemVer rules, and automatically writes the CHANGELOG. Because these files are committed alongside PRs, the intent behind each release is naturally preserved in the git history.
SemVer (Semantic Versioning) A versioning convention in
major.minor.patchformat. Breaking changes increment major, new features increment minor, and bug fixes increment patch.
The 3-Step Workflow
The core flow is straightforward.
# 1. Describe the change — creates a .changeset/*.md file
pnpm changeset
# 2. Bump versions + update CHANGELOG
pnpm changeset version
# 3. Publish to npm
pnpm changeset publishIn practice, step 1 is done manually by each developer per PR, while steps 2 and 3 are handled automatically by GitHub Actions.
Integration with pnpm Workspaces
Changesets reads pnpm-workspace.yaml to automatically understand inter-package dependencies. The updateInternalDependencies setting controls this: it determines the minimum bump level applied to package B when the version of an internal package A it depends on is incremented. With the default value of "patch", when A gets a patch bump, the version range for A in B's package.json is automatically updated on a patch basis. Not having to manually track dependent packages makes this especially useful in a monorepo.
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'Practical Application
Example 1: From Initial Setup to Creating Your First Changeset
Start by installing the CLI at the root and running initialization. With recent versions of pnpm, the -w flag is required when installing at the workspace root.
pnpm add -D -w @changesets/cli
pnpm changeset initInitialization creates .changeset/config.json. Here are the key options you'll find yourself touching most often.
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
"access": "restricted"is the default. If you're publishing a scoped package (one starting with@, like@myorg/ui) publicly, you must change this to"public". Attempting to publish with this setting left as-is will fail with an access permission error.
I remember skipping past this setting after my first init and running into an error on the very first deployment.
| Option | Role | Notes |
|---|---|---|
changelog |
Specifies CHANGELOG generation format | Replace with @changesets/changelog-github to include GitHub links |
access |
npm publish access level | Change to "public" when publishing scoped packages publicly |
updateInternalDependencies |
Minimum bump type propagated to dependent packages when an internal package version increases | Choose "patch" or "minor" |
baseBranch |
Base branch for PR comparisons | Defaults to main; update if using a different strategy |
Running pnpm changeset opens an interactive CLI. You select which packages changed, choose the change type (major/minor/patch), write a one-line summary, and a randomly named markdown file is generated in the .changeset/ directory. Simply include this file in your PR commit and you're done.
Registering these as scripts in package.json ensures the whole team uses consistent commands.
{
"scripts": {
"changeset": "changeset",
"version-packages": "changeset version",
"release": "pnpm build && changeset publish"
}
}Example 2: Setting Up an Automated Deployment Pipeline with GitHub Actions
Honestly, this was the most confusing part for me at first. Even after reading the docs, it wasn't easy to grasp that changesets/action behaves differently depending on the situation.
# .github/workflows/release.yml
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@v3
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
id: changesets
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 }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}This workflow behaves differently in two scenarios.
| Scenario | Behavior |
|---|---|
When there are unprocessed files in .changeset/ |
Automatically creates or updates a PR titled "chore: version packages" |
| After that PR is merged into main | Runs pnpm release → publishes to npm |
The fetch-depth: 0 option is easy to overlook, but it's essential—without fetching the full tag history, version calculation can go wrong.
Examples 3 and 4 below are advanced features you can introduce as needed once you're comfortable with the basic setup. If you're applying Changesets for the first time, Examples 1 and 2 are sufficient.
Example 3: Preview Deployments with Snapshot Releases
This is useful when you want to run integration tests in CI before an official release. Instead of a real version number, a temporary version with a timestamp is published to npm.
pnpm changeset version --snapshot canary
pnpm changeset publish --tag canary
# Result: @myorg/ui@0.0.0-canary-20250512123456Versions published this way can be installed with the @myorg/ui@canary tag, letting apps that use the package validate changes at the PR stage before an official release.
One thing to watch out for: after running changeset version --snapshot, you generally don't want to commit the modified package.json files to the main branch. I didn't know this when I first used snapshot releases and ended up with snapshot versions landing in main. Since then, I either work in a separate branch or use git stash to keep snapshot version files out of the main branch.
Example 4: Managing Grouped Package Versions with Fixed Mode
If you have a UI component library split into React, Vue, and core packages and always want to release them together, you can use fixed mode.
{
"fixed": [["@myorg/core", "@myorg/react", "@myorg/vue"]]
}Even if only one of the three changes, the other two are bumped to the same version as well. Here's what the output looks like in practice.
// Before
// packages/core/package.json
{ "name": "@myorg/core", "version": "2.0.1" }
// packages/react/package.json (no direct changes)
{ "name": "@myorg/react", "version": "2.0.1" }
// Even with a minor changeset only on @myorg/core, the result is:
// packages/core/package.json
{ "name": "@myorg/core", "version": "2.1.0" }
// packages/react/package.json
{ "name": "@myorg/react", "version": "2.1.0" }This prevents users from getting confused by mixing @myorg/react@2.1.0 with @myorg/core@2.0.1. We were getting questions about version mismatches while running a UI library as separate packages, and they stopped entirely after switching to fixed mode.
Pros and Cons
Advantages
| Item | Details | Usage Tips |
|---|---|---|
| Change intent is preserved in git | Changeset files are committed with PRs, making it always traceable "why a version was bumped" | Use @changesets/changelog-github to include PR links in the CHANGELOG |
| Prevents missing dependent package version bumps | When an internal package changes, dependent package versions are automatically bumped | Setting updateInternalDependencies to "minor" enables more conservative management |
| CHANGELOG fills itself | Per-package CHANGELOG.md is automatically generated and updated when changeset version runs |
Changeset summaries become entries instead of commit messages, naturally raising message quality |
| Release preparation is distributed across PRs | Version management work that one person used to do in bulk is naturally distributed across each developer's PRs | Checking for missing changeset files in CI makes it easy to maintain team discipline |
| Supports various release scenarios | Choose from fixed, linked, snapshot, prerelease, and other modes to fit the situation | The default mode is sufficient early on; introduce advanced modes incrementally as needed |
| Independent of build tooling | Works the same with Turborepo, Nx, or a plain pnpm workspace | Switching build tools later doesn't affect the release pipeline |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| A changeset file must be added per PR | If one is forgotten, automation stops | Use the hasChangesets output from changesets/action to check for PRs missing a changeset |
| Initial setup requires significant effort | Learning costs for GitHub Actions, npm tokens, and repository permissions | Recommended to start by copying the example workflow from this article |
| Can slow down with many packages | changeset version execution time increases with dozens or more packages |
Exclude packages that don't need publishing using the ignore setting |
| Pre-release management is complex | Managing alpha/beta/rc requires a separate branch strategy | Refer to the official pre-release mode guide in the documentation |
The Most Common Mistakes in Practice
-
Adding
.changeset/to.gitignore— Changeset files must be tracked by git. The instinct of "these are auto-generated files, so they should be ignored" leads to adding them to.gitignore, which meanschangeset versionhas nothing to read and nothing happens. -
Omitting
fetch-depth: 0in the GitHub Actions checkout — The default is a shallow clone, so git tag history is unavailable. This can cause unexpected errors in parts of the process that rely on version tags, so it's important to always include it. -
Not running the build first in
pnpm release—changeset publishdeploys whatever build artifacts are present. Using just"release": "changeset publish"can result in pre-build state being published, so it's better to structure it aspnpm build && changeset publish.
Closing Thoughts
Changesets is designed to delegate the responsibility of version management to tooling, while keeping the recording of change intent in the hands of the developer. The burden of manually editing versions across multiple package.json files and having someone write the CHANGELOG all at once is absorbed into the pipeline.
Three steps you can take right now:
- Run
pnpm add -D -w @changesets/cli && pnpm changeset initat the root of your existing pnpm monorepo. Once.changeset/config.jsonis created, it's enough to just update theaccessvalue andbaseBranchto match your project. - The next time you open a PR, run
pnpm changesetonce and commit the changeset file along with it—you'll immediately see how intuitive the file contents are. - Once you're comfortable with the basic behavior, paste the GitHub Actions example from this article into
.github/workflows/release.ymland set theGITHUB_TOKENandNPM_TOKENsecrets, and publishing will be fully automated.
References
- Using Changesets with pnpm | pnpm Official Docs
- changesets/changesets | Official GitHub Repository
- Changesets Official Documentation Site
- changesets/action | Official GitHub Action
- Config File Options | Changesets Official Docs
- Snapshot Releases | Changesets Official Docs
- Changesets for Versioning | Vercel Academy
- Complete Monorepo Guide: pnpm + Workspace + Changesets (2025) | jsdev.space
- Monorepo Architecture with pnpm Workspace, Turborepo & Changesets | DEV Community
- Automate changelog generation and publish with Changesets | DEV Community
- Frontend Handbook: Changesets | Infinum
- Introducing Changesets: Simplify Project Versioning | Liran Tal