Automating Monorepo Releases with pnpm Workspace + Changesets — Including GitHub Actions CI Pipeline Setup
Do you know what gave me the biggest headache when I first adopted a monorepo? Not the build, not the tests — it was the confusion around release timing: "I bumped this package's version, but do I need to bump that one too?" If you bump @myorg/ui to 0.5.0, what about @myorg/utils? What about @myorg/app? When everyone on the team manually edits package.json, you end up with CHANGELOGs in all different formats and tracking down "why did this change go in?" turns into a game of git blame detective work.
This post is a practical breakdown of the pnpm Workspace + Changesets combination for those who are already using pnpm workspaces and are thinking about release automation for the first time. By the end of this post, you'll be able to set up a GitHub Actions pipeline that automates version bumping, CHANGELOG generation, and npm publishing — all from a single changeset file.
Back in my Lerna days, touching one config file wrong could cost me half a day. After switching to Changesets, that fear largely disappeared. I honestly can't count how many times I thought, "Why didn't I switch sooner?"
Core Concepts
pnpm Workspace — The Foundation That Ties Internal Packages Together
pnpm Workspace is a built-in pnpm feature that lets you manage multiple packages together within a single repository. Just place a single pnpm-workspace.yaml file at the root and you can connect your packages into one workspace.
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'When declaring dependencies between internal packages, use the workspace:* protocol. When you later publish to npm, this workspace:* is automatically replaced with the actual version number (something like ^1.2.0). No need to manually align versions.
{
"name": "@myorg/app",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*"
}
}workspace: protocol substitution*: When running
pnpm changeset publishorpnpm -r publish, pnpm automatically substitutes the current package version number. You can narrow the range withworkspace:^orworkspace:~, but for internal packages it's usually easiest to just follow the latest with*. Note that there are edge cases wherepnpm -r publishhandles the substitution more reliably thanchangeset publish, so if you run into issues, try switching the publish command.
Changesets — The Philosophy of Separating "Change Intent" from "Release"
The core philosophy of @changesets/cli is simple: separate the moment you record a change's intent from the moment you actually release it. In the PR where you build a feature, you commit a changeset file describing "what packages this change affects and to what degree," and when you're ready to release, you consume all those files at once to bump versions and generate CHANGELOGs.
The workflow breaks down into three steps:
| Step | Command | Description |
|---|---|---|
| Record changes | pnpm changeset |
Creates .changeset/*.md files |
| Apply versions | pnpm changeset version |
Consumes changeset files → bumps package.json versions + generates CHANGELOGs |
| Deploy | pnpm changeset publish |
Publishes changed packages to npm |
A changeset file looks like this:
---
"@myorg/ui": minor
"@myorg/utils": patch
---
Add variant prop to Button component, fix bug in util functionThe frontmatter declares which packages are affected and the semver range, and the body contains a human-readable description of the changes.
semver guidelines: major is for breaking changes, minor for backwards-compatible new features, patch for bug fixes. Changesets delegates this judgment to the PR author. This is also the biggest difference from semantic-release, which analyzes commit messages automatically.
A common real-world scenario: when multiple changesets exist for the same package simultaneously, changeset version merges them at the highest semver level. For example, if @myorg/ui has both a minor and a patch changeset at the same time, they're combined into a minor bump. This is why you don't need to worry when multiple PRs pile up at once.
When you run changeset version, the changeset files disappear and their contents are consolidated into each package's CHANGELOG.md. The result looks like this:
# @myorg/ui
## 0.6.0
### Minor Changes
- Add variant prop to Button component
## 0.5.1
### Patch Changes
- Updated dependenciesNow that we understand the concepts, let's put them into practice.
Practical Application
Initial Setup
Whether it's a new monorepo or an existing pnpm workspace, adopting Changesets is simpler than you'd think. Just two commands from the root and you're done.
# Run from the root (installs as a devDependency at the workspace root)
pnpm add -Dw @changesets/cli
# Initialize
pnpm changeset init
# → Creates .changeset/config.jsonHere's the full view of the generated .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}Let's highlight the options worth paying attention to:
| Option | Default | Description |
|---|---|---|
access |
"restricted" |
Whether the npm package is public. Must be changed to "public" to publish scoped packages (@myorg/...) to public npm |
updateInternalDependencies |
"patch" |
When an upstream package is updated, downstream packages are automatically patch bumped (cascade bump) |
ignore |
[] |
List of packages to exclude from version management |
commit |
false |
Whether to automatically commit when running changeset version |
cascade bump: With
updateInternalDependencies: "patch", for example, patch-updating@myorg/utilswill automatically patch-bump@myorg/uiif it depends on@myorg/utils. This setting automatically prevents version mismatches between internal packages.
The access option is worth checking carefully during initial setup. I personally spent an hour wondering why publishing wasn't working, only to realize I'd forgotten to change it from "restricted" to "public". Since the default is "restricted", it's easy to miss when publishing public packages.
If you want to automatically include PR numbers and contributor links in your CHANGELOG, you can install @changesets/changelog-github:
pnpm add -Dw @changesets/changelog-githubThen update the full config.json like this:
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "myorg/myrepo" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}Adding Changesets During Development
After finishing a feature and before opening a PR, run pnpm changeset in the terminal and an interactive CLI will appear.
pnpm changesetSelect which packages are affected, choose between major/minor/patch, enter a one-line description of the change, and a markdown file with a random name will be created in the .changeset/ directory. Just include this file along with your code when you commit the PR.
---
"@myorg/ui": minor
---
Add `size` prop to Button component for xs/sm/md/lg variantsWhen PRs slip through without a changeset file, the automation breaks down — and this is where changesets-bot helps. Install it as a GitHub App (github.com/apps/changeset-bot) and when a PR is missing a changeset file, the bot automatically comments to let you know. I'd recommend installing this bot before rolling it out to the team.
If you want to enforce this more strictly in CI, you can use pnpm changeset status:
# Add to PR CI workflow — fails CI if changeset is missing
- name: Check changesets
run: pnpm changeset status --since=origin/mainIf there are no changeset files at all, this command returns an error and the PR won't pass.
Fully Automating Releases with GitHub Actions
This is the most powerful part. Using changesets/action, whenever changeset files land on the main branch, a "Version Packages" PR is automatically created, and the moment that PR is merged, npm publishing happens automatically too.
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full git history required — the action misbehaves without it
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version
title: "chore: release packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Generate NPM_TOKEN from the Access Tokens page on your npmjs.com account, then register it as NPM_TOKEN in your GitHub repository under Settings → Secrets and variables → Actions.
Here's what this workflow actually does:
| Situation | Action behavior |
|---|---|
.changeset/*.md files exist on main |
Automatically creates a "Version Packages" PR (includes version bump + CHANGELOG preview) |
| "Version Packages" PR is merged | Runs pnpm changeset publish → automatically publishes to npm |
Team members just need to include a changeset file in their PR. Release timing is decided by merging the "Version Packages" PR, which makes the release manager role incredibly simple.
Running Alpha/Beta Pre-releases
This is the pattern to use when you want to ship on an alpha channel before stabilizing a new major version.
# Enter alpha channel (creates pre.json)
pnpm changeset pre enter alpha
# Record changes as changesets in this state
pnpm changeset
# Apply versions (bumps to 1.0.0-alpha.0 format)
pnpm changeset version
# Once sufficiently validated, exit the channel
pnpm changeset pre exit
# Switch to stable version
pnpm changeset versionThe pre-release state is stored in .changeset/pre.json, and committing this file to Git means the entire team shares the same state. Other channel names like beta and rc can be used freely as well.
Pros and Cons
Pros
| Item | Details |
|---|---|
| PR-level change documentation | Since you record the reason for changes at the time of feature development, there's no need to rack your brain right before a release |
| Independent version management | Maintains independent semver per package. A major update to @myorg/ui doesn't affect @myorg/utils |
| Automatic cascade bump for internal dependencies | When an upstream package is updated, downstream packages are automatically patch bumped, preventing version mismatches |
| Tool agnosticism | Works with Turborepo, Nx, or plain pnpm workspaces. Not tied to any specific build system |
Cons and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Depends on team habits | If anyone skips a changeset in a PR, the automation breaks down | Install changesets-bot GitHub App + enforce with changeset status in CI |
| workspace:* substitution issues | There are edge cases where changeset publish fails to handle the workspace: protocol |
Use pnpm -r publish or set "bumpVersionsWithWorkspaceProtocolOnly": true in config |
| Pre-release complexity | The pre.json state file must be managed in Git, and there's a risk of conflicts during channel transitions |
Clearly document the team's pre-release procedure |
| Unintended version bumps | In pre-release mode, packages with no changes may still get bumped | Exclude specific packages with the ignore option |
The Most Common Mistakes in Practice
-
The habit of opening PRs without a changeset — When first adopting this workflow, if the whole team doesn't understand the flow, PRs will slip through without changesets. Installing changesets-bot first helps with habit formation, as the bot will kindly notify via comments.
-
Leaving
access: "restricted"as-is — Publishing scoped packages (@myorg/...) to public npm requires changing it to"public". Since the default is"restricted", it's best to check this first during initial setup. -
Attempting to publish a stable release without
pre exitwhile in a pre-release channel — If you run a regularchangeset versionwhilepre.jsonstill exists, the version will come out differently than expected. It's good for everyone on the team to know the procedure of runningpre exitbefore switching to a stable version.
Closing Thoughts
The pnpm Workspace + Changesets combination is a tool that "turns releases from an event into a process." After setup, team members just need to include a changeset file in their PR, and releasing becomes as simple as merging the "Version Packages" PR.
Three steps you can start right now:
-
Run
pnpm add -Dw @changesets/cli && pnpm changeset init— In 5 minutes you'll have.changeset/config.json. Just adjust theaccessoption to fit your team's needs and the basic setup is done. -
From your next PR onward, include a changeset file with
pnpm changeset— It might feel unfamiliar at first, but after two or three times it becomes natural. Trying it yourself first to get a feel for the flow is also a good approach. -
Add the GitHub Actions workflow above to
.github/workflows/release.ymland register theNPM_TOKENsecret — From then on, every time a changeset file is merged into main, a "Version Packages" PR will be automatically created.
If you get stuck, you can get community help at Changesets GitHub Discussions. It's more active than you might expect, and similar cases are often already being discussed.
Next post: Now that you've automated releases with Changesets, it's time to cut your build times — we'll cover a practical guide to reducing monorepo CI build times with Turborepo's remote caching and task pipelines.
References
Essential References
- Using Changesets with pnpm | pnpm official docs
- changesets/changesets | GitHub
- Changesets official docs
- changesets/action — Official GitHub Actions action | GitHub
Configuration & Deep Dives
- config-file-options.md — Config file options reference | GitHub
- prereleases.md — Pre-release guide | GitHub
- Changesets for Versioning | Vercel Academy
- Guide to version management with changesets | LogRocket Blog
Real-world Examples
- Complete Monorepo Guide: pnpm + Workspace + Changesets (2025) | jsdev.space
- Monorepo Architecture with pnpm Workspace, Turborepo & Changesets | DEV Community
- Why we stopped using Lerna for monorepos | DEV Community
Troubleshooting