Automating Releases with Changesets
Set up @changesets/cli so contributors declare release intent in a small file per pull request, and a bot batches those intents into a versioned, changelog-backed release — for a single package or an entire monorepo.
When to Use This
Reach for changesets when any of these are true:
- You maintain a monorepo where packages version independently and a single change may touch several of them.
- You want the release decision reviewable in a pull request before anything publishes.
- Your team will not reliably follow a strict commit-message convention, so inferring the bump from commits is fragile.
- You need preview/snapshot builds (e.g.
0.0.0-pr-42-20260619) for testing before a real release.
If instead you have a single package and already enforce Conventional Commits, the unattended commit-driven path in Configuring Conventional Commits and semantic-release is lighter weight.
How a Changeset Works
A changeset is a markdown file in .changeset/ with frontmatter listing each affected package and its bump level, plus a human summary:
---
"@scope/widget": minor
"@scope/utils": patch
---
Add a `variant` prop to Widget and fix a rounding bug in utils.
The bump level (patch, minor, or major) follows the same semantic-versioning rules used everywhere in Semantic Versioning and Release Automation: classify by public-API impact. When the version command runs, changesets aggregates every pending file, applies the highest bump per package, propagates bumps to internal dependents, writes the new versions into each package.json, appends to each CHANGELOG.md, and deletes the consumed changeset files.
Setup Steps
- Install the CLI as a dev dependency at the repo root:
npm install --save-dev @changesets/cli - Initialize the config and
.changeset/directory:npx changeset init - Configure
.changeset/config.json. For a public monorepo:{ "$schema": "https://unpkg.com/@changesets/config/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@scope/internal-example"] }access: "public"is required for scoped packages;updateInternalDependenciescontrols how dependents are bumped when a workspace package changes;ignoreexcludes private apps that should never publish. - Add release scripts to the root
package.json:{ "scripts": { "changeset": "changeset", "version-packages": "changeset version", "release": "changeset publish" } }changeset versionconsumes intent files and writes bumps;changeset publishbuilds and publishes anything whose version is ahead of the registry, creating Git tags per package. - Author a changeset whenever you make a user-facing change:
Commit the generatednpx changeset # interactive: pick packages, pick bump level, write a summary.changeset/<name>.mdalongside your code change.
CI Integration
The official changesets GitHub Action handles both halves automatically: on a normal push it opens or updates a "Version Packages" pull request; when that PR merges, it runs the release command and publishes.
# .github/workflows/release.yml
name: release
on:
push:
branches: [main]
concurrency: release-${{ github.ref }}
permissions:
contents: write # create the version PR, commits, and tags
pull-requests: write # open/update the release pull request
id-token: write # OIDC for npm provenance
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- run: npm ci
- run: npm run build # build before publish via the release script
- uses: changesets/action@v1
with:
version: npm run version-packages # opens/updates the version PR
publish: npm run release # publishes when versions lead the registry
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Snapshot Releases
To publish a throwaway preview from a feature branch without touching the changelog or latest:
# version every package with pending changesets to a snapshot id
npx changeset version --snapshot pr-42
# publish under a non-latest tag so it can never become the default install
npx changeset publish --tag pr-42 --no-git-tag
Consumers test it with npm install @scope/widget@pr-42. Snapshots require at least one pending changeset; without one, changesets has nothing to version.
Validation Commands
# Preview what would be released without writing anything
npx changeset status --verbose
# Confirm the version step produced clean bumps and changelogs (inspect the diff)
npx changeset version && git diff --stat
# Dry-run the publish to see which packages would ship
npx changeset publish --dry-run
# Verify tags were created after a real release
git tag --list '@scope/*'
Prevention & CI Guardrails
- Add a CI check that fails a pull request touching
src/but containing no changeset, so user-facing changes can never ship unversioned. - Keep
access: "public"in the config for scoped packages — the most common cause of a silent first-publish failure is leaving it private. - List every non-publishable app in
ignoreso a private workspace package never gets versioned or published by accident. - Set
fetch-depth: 0on checkout; changesets needs full history to diff againstbaseBranch. - Use a scoped, short-lived
NPM_TOKENand run the publish only frommainbehind aconcurrencyguard.
- Require a changeset on any PR that modifies a publishable package, enforced by
changeset status. - Use the changesets GitHub action to open and manage the release PR automatically.
- Add an empty changeset for deliberately non-releasing changes.
- Configure
updateInternalDependenciesso a change propagates to dependent packages.
The release PR workflow
The distinctive part of Changesets automation is the release pull request, which separates accumulating changes from cutting a release. As changes merge to the main branch with their changesets, the changesets action maintains an open 'Version Packages' pull request that shows exactly what the next release will contain — the version bumps and the assembled changelog entries. Merging that PR applies the version changes and triggers the publish, so cutting a release is a single, reviewable action rather than a command run from a laptop.
This workflow gives a team control over release timing while keeping the mechanics automatic. Changes can accumulate on main for days, each adding to the pending release PR, and the release goes out when someone merges it — which might be on a schedule, at a milestone, or whenever enough has accumulated. The PR itself is the review artifact: it shows every package's next version and changelog before anything is published, so a surprising bump or a missing entry is caught before the release rather than after. Between the per-change changesets and the aggregating release PR, the process makes both what is released and when it is released explicit and reviewable, which is the core appeal of the intent-driven model for a monorepo.
Versioning internal dependencies correctly
In a monorepo, a change to one package often requires bumping the packages that depend on it, and Changesets handles this through its internal-dependency configuration. When you add a changeset for @acme/core, Changesets can automatically bump the dependents that reference it so a consumer of @acme/ui receives a version that pulls in the fixed @acme/core. The updateInternalDependencies setting controls whether this propagation happens on patch-level changes or only on minor-and-above.
Getting this configuration right prevents a subtle release gap where a dependency changes but its dependents are not republished, leaving consumers on a version that references a stale internal package. Aligning the threshold with how tightly your packages couple — patch-level propagation for tightly-linked packages, minor-and-above for more independent ones — ensures a change reaches the consumers whose behavior it affects. This is the monorepo-specific complement to the basic requirement that each changed package have a changeset: not only must the changed package be released, but the dependents whose output the change affects must be bumped too, which is exactly what the automated internal-dependency versioning provides.
Enforcing changesets in CI
The one failure mode of the Changesets model — a change merged without a changeset, producing no release — is prevented by enforcing the requirement in CI. The changeset status --since=<base> command exits non-zero when a publishable package changed without an accompanying changeset, so making it a required check turns a silent no-op release into a red build on the pull request that introduced the gap. The contributor is prompted to run pnpm changeset and describe the bump before merge, so the intent is captured with the change rather than remembered later.
Deliberately non-releasing changes are handled by adding an empty changeset, which explicitly records 'no release needed' rather than leaving the intent ambiguous — a refactor or a docs edit passes the check with an empty changeset. This makes the intent-driven model self-enforcing: every change either carries a changeset describing its release impact or an empty one declaring it has none, and the CI gate ensures one of the two is always present. Without the check, the model relies on everyone remembering the extra step, which is exactly the discipline that erodes under deadline pressure; with it, a forgotten changeset cannot reach the main branch, so the release is always complete.
Prerelease and snapshot releases with Changesets
Changesets supports prerelease and snapshot modes for the cases where the standard release PR flow is not what you want. Entering prerelease mode with changeset pre enter next makes subsequent changeset version runs produce x.y.z-next.N versions, so you can publish a release candidate line under a next tag and validate it before exiting prerelease mode and cutting the stable release. This gives the same staged-rollout capability that dist-tags provide, driven by the accumulated changesets.
Snapshot releases serve a different need: publishing an ephemeral version from a pull request for testing, without affecting the normal versioning. changeset version --snapshot pr-123 produces a throwaway version like 0.0.0-pr-123-20240101 that a reviewer can install to test the PR's changes in a real consumer, and which never becomes part of the stable release history. Between prerelease mode for staged rollouts and snapshot releases for PR testing, Changesets covers the release scenarios beyond the standard merge-the-release-PR flow, so a team can validate risky or in-progress changes with real installs while keeping the stable release process clean and intent-driven.
Frequently Asked Questions
Do I need a changeset for every commit?
No — only for changes that affect what consumers install. Internal refactors, test-only changes, and CI tweaks need none. Add the empty-changeset marker (npx changeset --empty) if you want to record explicitly that a change is release-irrelevant.
How does changesets pick the version when several changesets target the same package? It applies the highest bump level among them. Three patches and one minor for the same package produce a single minor release, and all of their summaries are collected into that version's changelog entry.
Why did changeset publish skip a package?
Publish only ships packages whose local version is ahead of what is on the registry. If the version step did not run (no pending changesets) or the package is listed in ignore or marked private, it is correctly skipped.
Can I use changesets with pnpm or Yarn workspaces? Yes. Changesets reads the workspace definition from your package manager, so it works with npm, pnpm, and Yarn workspaces; just run the CLI from the repo root and ensure the install step uses your package manager's frozen install.
How does the Changesets release PR work?
As changes merge with their changesets, the changesets action maintains an open 'Version Packages' PR showing the next release's version bumps and changelog. Merging that PR applies the versions and publishes, so cutting a release is a single reviewable action and you control the timing.
Why didn't a dependent package get released when its dependency changed?
Changesets bumps internal dependents based on the updateInternalDependencies threshold. If it is set to minor-and-above and you made a patch change, dependents are not bumped. Lower the threshold for tightly-coupled packages so a change propagates to the consumers that need it.
How do I make sure every change has a changeset?
Add a required CI check running changeset status --since=origin/main, which fails a PR that changes a publishable package without a changeset. For a deliberately non-releasing change, add an empty changeset so the intent — no release — is explicit and the check passes.
How do I publish a prerelease or a test version with Changesets?
Use changeset pre enter next for a prerelease line that produces x.y.z-next.N versions under a next tag, or changeset version --snapshot pr-123 for a throwaway version to test a PR in a real consumer. Both keep the stable release history clean.
Does Changesets work outside of GitHub?
Yes — the core CLI (changeset, changeset version, changeset publish) is platform-agnostic and runs in any CI. The changesets GitHub action automates the release PR specifically, but on other platforms you can script the version-and-publish steps directly.
Related
- Configuring Conventional Commits and semantic-release — the commit-driven alternative for single-package repos.
- npm Registry Publishing Workflows — authentication and access that the publish step depends on.
- Lockfile Management Strategies — frozen installs that keep CI releases reproducible.