changesets
Changesets Best Practices
Overview
Changesets is a file-based approach to versioning and changelog management. Instead of deciding versions at release time, contributors declare their changes as they work — each change gets a markdown file describing what changed and the semver bump type.
Setup
npm install -D @changesets/cli
npx changeset init
This creates a .changeset/ directory with a config.json.
Configuration
// .changeset/config.json
{
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
}
| Option | Description |
|---|---|
changelog |
Changelog generator (use @changesets/changelog-github for PR/author links) |
commit |
Auto-commit version bumps and changelog updates |
fixed |
Groups of packages that always share the same version |
linked |
Groups of packages whose versions stay in sync for matching bump types |
access |
"public" for public npm, "restricted" for private |
baseBranch |
Branch that releases are made from |
updateInternalDependencies |
How to bump dependents when a dependency changes |
ignore |
Packages to exclude from changesets |
Core Workflow
1. Add a Changeset
When you make a change worth documenting:
npx changeset
This prompts you to:
- Select which packages are affected.
- Choose the semver bump type for each (
major,minor,patch). - Write a summary of the change.
It creates a markdown file in .changeset/:
---
"@myorg/ui": minor
"@myorg/utils": patch
---
Add new DatePicker component with timezone support.
Updated date formatting utilities to handle timezone offsets.
2. Version Packages
When ready to release:
npx changeset version
This:
- Consumes all changeset files in
.changeset/. - Bumps
package.jsonversions based on the highest bump type per package. - Updates
CHANGELOG.mdfor each affected package. - Bumps internal dependents as needed.
3. Publish
npx changeset publish
Publishes all packages with new versions to npm. Run after committing the version bump changes.
Writing Good Changeset Summaries
Changeset summaries end up in your CHANGELOG.md — write them for consumers, not for yourself:
## <!-- Good: explains the impact -->
## "@myorg/button": minor
Add `loading` prop to Button component. When `true`, displays a spinner
and disables the button to prevent duplicate submissions.
## <!-- Bad: describes implementation, not impact -->
## "@myorg/button": minor
Added useState for loading state and conditional rendering of Spinner component.
Guidelines
- Write in imperative mood: "Add feature" not "Added feature".
- Focus on what changed from the consumer's perspective.
- Mention breaking changes explicitly and include migration steps.
- One changeset per logical change — don't bundle unrelated changes.
- Include code examples for API changes:
---
"@myorg/config": major
---
Change `createConfig` to accept an options object instead of positional arguments.
Before:
\`\`\`ts
createConfig("production", true, 3000);
\`\`\`
After:
\`\`\`ts
createConfig({ env: "production", debug: true, port: 3000 });
\`\`\`
Semver Conventions
| Bump | When | Examples |
|---|---|---|
patch |
Bug fixes, internal refactors, dependency updates | Fix null check, update devDependency |
minor |
New features, non-breaking additions | Add prop, export new utility, add optional parameter |
major |
Breaking changes | Remove/rename export, change function signature, drop Node version |
When in doubt between minor and patch, ask: "Would a consumer need to change their code?" If no, it's a patch.
GitHub Changelog Generator
Use @changesets/changelog-github for richer changelogs with PR links and author attribution:
npm install -D @changesets/changelog-github
// .changeset/config.json
{
"changelog": ["@changesets/changelog-github", { "repo": "grahamcrackers/my-package" }],
}
This produces changelogs like:
## 2.1.0
### Minor Changes
- Add DatePicker component ([#142](https://github.com/org/repo/pull/142)) — @grahamcrackers
CI Automation with GitHub Actions
Automated Version PRs and Publishing
Use the official changesets/action to automate the release workflow:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm ci
- uses: changesets/action@v1
with:
publish: npm run release
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
This action:
- Detects pending changesets on main.
- Opens a "Version Packages" PR that bumps versions and updates changelogs.
- When the PR is merged, publishes to npm automatically.
Require Changesets in PRs
Add a check that PRs include a changeset when source code changes:
# .github/workflows/changeset-check.yml
name: Changeset Check
on:
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx changeset status --since=origin/main
Empty Changesets
For PRs that don't need a version bump (docs, CI, tests), add an empty changeset:
npx changeset --empty
This creates a changeset with no packages selected, satisfying CI checks without triggering a release.
Monorepo Patterns
Fixed Versioning
Packages that should always share the same version:
{
"fixed": [["@myorg/core", "@myorg/cli", "@myorg/types"]],
}
When any package in the group bumps, all packages bump to the same version.
Linked Versioning
Packages whose versions stay in sync for the same bump type, but can diverge:
{
"linked": [["@myorg/react", "@myorg/vue", "@myorg/svelte"]],
}
If @myorg/react gets a minor bump, all linked packages that also have changesets get the same minor version.
Internal Dependencies
When package A depends on package B and B gets a major bump:
- A automatically gets a patch bump (configurable via
updateInternalDependencies). - A's dependency on B is updated to the new version.
Ignoring Packages
Exclude packages that aren't published (apps, internal tooling):
{
"ignore": ["@myorg/web-app", "@myorg/internal-scripts"],
}
Snapshot Releases
Publish pre-release versions for testing without consuming changesets:
npx changeset version --snapshot canary
npx changeset publish --tag canary
This produces versions like 0.0.0-canary-20260224 and publishes under the canary npm tag so npm install @myorg/ui still gets the stable release.
Pre-releases
For alpha/beta/rc release channels:
npx changeset pre enter beta
# Now all `changeset version` calls produce beta versions (e.g., 2.0.0-beta.0)
# Continue adding changesets and versioning as normal
npx changeset pre exit
# Exit pre-release mode and release stable versions
File Structure
.changeset/
├── config.json # configuration
├── README.md # auto-generated docs
├── fuzzy-dolphins-arrive.md # pending changeset
└── brave-tigers-dance.md # pending changeset
Changeset filenames are randomly generated — commit them as-is. They're consumed and deleted when you run changeset version.