Semantic Versioning and Automated Releases in Monorepos
Manual versioning in a monorepo is error-prone: someone forgets to bump the version, another publishes the wrong package, or two packages get the same version number. Semantic versioning (MAJOR.MINOR.PATCH) standardizes this. Changesets automate the process: developers create changeset files, CI generates changelogs, bumps versions based on changes, and publishes. This article shows you how to set up Changesets, write changeset files, and automate releases.
Understanding Semantic Versioning
Semantic versioning follows three rules:
- MAJOR: breaking changes (e.g., removing a function, changing a prop name).
- MINOR: backward-compatible features (e.g., adding an optional prop).
- PATCH: backward-compatible bug fixes (e.g., fixing a color value).
Format: MAJOR.MINOR.PATCH, e.g., 1.2.3. When you release, bump only the most relevant number. If you fix a bug, release patch 1.2.4. If you add a feature, release minor 1.3.0. If you break the API, release major 2.0.0.
Installing and Initializing Changesets
Install Changesets at the monorepo root:
pnpm add -D @changesets/cli @changesets/changelog-github
pnpm exec changeset init
This creates .changeset/config.json:
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "mycompany/monorepo" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Key fields:
changelog: tool to generate changelogs (GitHub markdown links).access: "public" for public packages, "private" for private scopes.baseBranch: branch to compare against for affected detection.
Creating Changeset Files
Before merging a PR, create a changeset describing your change:
pnpm exec changeset
Follow the prompts:
[?] Which packages would you like to include? (use arrow keys)
◯ @myapp/design-tokens
◉ @myapp/ui-library
◯ web
Select the packages you changed. Then:
[?] What kind of change is this for @myapp/ui-library? (Use arrow keys)
◯ major
◯ minor
◉ patch
[?] Please enter a summary for this change:
› Fixed Button component color on hover
Changesets creates a file like .changeset/gentle-pandas-play.md:
---
"@myapp/ui-library": patch
"@myapp/design-tokens": patch
---
Fixed Button component text color on hover for accessibility (WCAG AA compliance).
Commit this file with your PR. Do not bump versions manually; Changesets does it.
Running the Version Workflow
When you are ready to release, run the version command:
pnpm exec changeset version
Changesets reads all .changeset/*.md files and:
- Bumps package versions based on the type (major/minor/patch).
- Deletes the changeset files.
- Generates a
CHANGELOG.mdin each package directory. - Updates
package.jsonversion fields.
Example CHANGELOG.md:
# @myapp/ui-library
## 1.1.0
### Patch Changes
- Fixed Button component text color on hover for accessibility (WCAG AA compliance).
### Updated Dependencies
- `@myapp/[email protected]`
---
## 1.0.0
### Features
- Initial release with Button, Card, and Input components.
Commit the changes:
git add .
git commit -m "chore: version bump [skip ci]"
Publishing Packages
After versioning, publish to npm:
pnpm exec changeset publish
Changesets publishes all updated packages to npm and creates git tags for each version (e.g., @myapp/[email protected]).
Automating Releases in CI
Create a GitHub Actions workflow to automate the version and publish cycle. Create .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
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build
run: turbo build
- name: Test
run: turbo test
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: pnpm exec changeset publish
version: pnpm exec changeset version
commit: "chore: version bump"
title: "chore: version bump"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
This workflow:
- On every push to main, checks if there are changeset files.
- If yes, creates a "Release" PR with versioning changes.
- When the Release PR is merged, publishes to npm.
Developers create changesets in their feature branches. When the PR is merged, CI creates a Release PR. Merge the Release PR to publish.
Managing Interdependencies
If package A depends on package B, and B gets a major version bump, A should also bump at least minor. Configure this in .changeset/config.json:
{
"linked": [
["@myapp/ui-library", "@myapp/design-tokens"]
],
"updateInternalDependencies": "minor"
}
Now, if design-tokens bumps major, ui-library also bumps at least minor.
Skipping Releases for Documentation Changes
Some changes (docs only, README updates) do not need a version bump. Mark files to ignore in .changeset/config.json:
{
"ignore": ["docs/**", "*.md"]
}
Or use the [skip changesets] comment in your PR to skip creating a changeset when you forget.
Monitoring Release Status
View release history:
npm view @myapp/ui-library
Lists all published versions and publish dates. Or check GitHub Releases:
https://github.com/mycompany/monorepo/releases
Each release shows the tag, changelog, and publish date.
Key Takeaways
- Use semantic versioning (MAJOR.MINOR.PATCH) to signal the impact of changes.
- Create changeset files (
pnpm exec changeset) before merging code to document breaking/feature/fix changes. - Run
pnpm exec changeset versionto bump versions and generate changelogs. - Run
pnpm exec changeset publishto publish all updated packages to npm. - Automate the release process with GitHub Actions so releases happen on a schedule or on demand.
- Use
updateInternalDependenciesto auto-bump dependent packages when their dependencies change.
Frequently Asked Questions
What if I merge a PR without creating a changeset?
Changesets does not force changesets but warns you. Best practice: use a CI check to require a changeset for all PRs that modify code.
Can I create multiple changesets for a single PR?
Yes, but avoid it. One changeset per PR is the norm. If your PR covers multiple unrelated changes, split the PR.
How do I backport a fix to an old major version?
Create a branch from the old version tag, cherry-pick the fix, create a changeset, and release as a new patch. Example:
git checkout @myapp/[email protected]
git checkout -b fix/old-version
git cherry-pick <commit>
pnpm exec changeset
pnpm exec changeset version
pnpm exec changeset publish
What if a changeset describes the wrong change?
Delete the .changeset/*.md file and create a new one. Changesets has not run yet, so you can adjust.
How do I publish a prerelease (alpha/beta)?
Create a prerelease branch and release with a tag:
pnpm exec changeset version --snapshot alpha
pnpm exec changeset publish --tag alpha
This publishes as 1.1.0-alpha.0, letting users test without affecting the stable release.