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.
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.
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.