Versioning a monorepo with changesets Jump to heading

Versioning one package is a decision made at release time: look at what changed, pick major, minor or patch. In a monorepo with twenty packages, nobody can reconstruct that at release time β€” which packages changed, how significantly, and which dependents need bumping too. The changesets approach moves the decision to the pull request, where the author knows. Each pull request adds a small file naming the packages it affects and the bump each needs, with a sentence for the changelog. A release step later reads all pending changeset files, computes versions, updates manifests and changelogs, and the tags follow. This page sets up the workflow with the changesets tool for JavaScript workspaces and shows how the same pattern works elsewhere, within monorepo branch topology.

When to use this approach Jump to heading

  • A monorepo publishes several packages with independent versions.
  • Version bumps and changelogs are decided at release time and often wrong or missing.
  • Packages depend on each other, so bumping one should bump its dependents’ ranges.
  • Per-package tags are in use, as in tagging packages independently in a monorepo.

Step 1 β€” Initialise changesets in the repository Jump to heading

The tool adds a .changeset/ directory with configuration. Pending changes will live there as small Markdown files until a release consumes them.

npm install --save-dev @changesets/cli
npx changeset init
cat .changeset/config.json
{
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

updateInternalDependencies: patch means that when a package is released, packages depending on it get their dependency range updated and a patch release of their own, so the published graph stays consistent.

Step 2 β€” Add a changeset in each pull request Jump to heading

Authors run one command, choose the packages and bump types, and write a summary. The result is a file with a random name β€” which, as with changelog fragments instead of one changelog, means pull requests never conflict over it.

npx changeset
# ? Which packages would you like to include? @acme/money, billing-api
# ? Which packages should have a major bump? (none)
# ? Which packages should have a minor bump? billing-api
# ? Summary: Round credit note totals half-even.
cat .changeset/brave-otters-sing.md
---
"@acme/money": patch
"billing-api": minor
---

Round credit note totals half-even.
The changesets release cycleEach pull request adds a changeset file naming packages and bump types. Files accumulate on main. A release pull request, opened by automation, consumes them: versions are bumped, dependents updated, changelogs written. Merging it publishes packages and creates per-package tags.PR + changesetauthor decides bumpAccumulate.changeset/*.mdRelease PRversions + changelogsMergereviewedPublish + tagpkg@versionthe bump is decided by the person who understands the change, at the time they make it

Step 3 β€” Require a changeset in CI Jump to heading

A pull request that changes a published package without a changeset will release nothing. Check for it, with an escape for changes that need no release.

jobs:
  changeset:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: npm ci
      - run: npx changeset status --since=origin/${{ github.base_ref }}

changeset status --since fails if packages changed since the base branch without a changeset covering them. For changes that genuinely need no release β€” tests, internal tooling β€” an empty changeset (npx changeset --empty) records the decision explicitly.

Step 4 β€” Let a release pull request apply the versions Jump to heading

Rather than versioning on every merge, automation keeps a single β€œVersion packages” pull request open, updated whenever new changesets land on main. It shows exactly what the next release will contain. Merging it is the release decision.

# .github/workflows/release.yml
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    permissions: { contents: write, pull-requests: write, id-token: write }
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: npm ci
      - uses: changesets/action@v1
        with:
          version: npx changeset version     # updates versions, dependents, CHANGELOG.md files
          publish: npx changeset publish     # publishes and creates pkg@version tags
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Release-time versioning against changesetsDeciding versions at release time means reconstructing many packages' changes after the fact and often missing dependents. Changesets record each decision in the pull request, compute dependent bumps automatically and show the whole next release as one reviewable pull request.Decide at releaseChangesetswho decides the bumprelease managerchange authordependent packagesoften missedupdated automaticallychangelog entriesreconstructedwritten with the changenext release visiblenorelease PRthe release PR turns 'what are we shipping?' into a diff someone can review

Step 5 β€” Handle pre-releases and stable lines Jump to heading

For release candidates, changesets has a pre-release mode that produces versions like 2.5.0-rc.0 from the same files. Enter it on a branch, release candidates as needed, and exit to produce the final versions.

npx changeset pre enter rc
npx changeset version          # 2.5.0-rc.0 for packages with pending changes
# … test, more changesets, version again β†’ rc.1 …
npx changeset pre exit
npx changeset version          # 2.5.0

Pre-release tagging in general is covered in tagging pre-releases and release candidates.

Two weeks of changesets to one releaseOver two weeks, several pull requests each add changeset files for the packages they touch. The release pull request is updated after every merge to show the cumulative versions and changelogs. On release day it is reviewed and merged, publishing three packages with new tags.PR #840money: patchday 1PR #846billing-api: minorday 4PR #851export-api: patchday 9Release PRupdated after each mergeday 9Merged3 packages taggedday 14the release PR is always a preview of what merging it would publish

Step 6 β€” Apply the pattern outside JavaScript Jump to heading

The tool is JavaScript-specific, but the pattern is not. Any monorepo can use change files naming packages and bump types, a CI check that requires them, and a release script that reads them, computes versions and tags name@version. Equivalents exist for several ecosystems; a small script is enough for many repositories.

# Minimal pattern: changes/<pr>.yml with { package: bump } and a summary
for f in changes/*.yml; do yq -r 'to_entries[] | select(.key != "summary") | "\(.key) \(.value)"' "$f"; done |
  sort | awk '{rank["patch"]=1; rank["minor"]=2; rank["major"]=3; if (rank[$2] > best[$1]) {best[$1]=rank[$2]; b[$1]=$2}} END {for (p in b) print p, b[p]}'

Step 7 β€” Review the release pull request properly Jump to heading

The release pull request is the one place where every pending version bump and changelog entry is visible together. Read it as a release review: are the bump levels right, are the changelog lines written for users, did any package get a major bump by mistake? Fix problems by editing the pending changeset files on main, and the release pull request updates itself.

gh pr view "$(gh pr list --head changeset-release/main --json number --jq '.[0].number')" --json files \
  --jq '.files[] | select(.path | endswith("package.json")) | .path'

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Do changeset files cause merge conflicts? Jump to heading

No. Each has a randomly generated name, so concurrent pull requests add different files. They are deleted together by the release step.

Can a changeset be edited after merging? Jump to heading

Yes, until it is consumed by a release. Edit the file on main through a normal pull request to change the bump or wording.

What happens to packages with no changesets? Jump to heading

Nothing: they keep their version and get no tag. Only internal dependency updates can bump a package without its own changeset, and only as configured.