Guide + free tool

Changesets, the release tool that ignores your commits

Setup, the file you write for every change, what happens to the packages that depend on the one you bumped, and the errors I got while running it on a test monorepo. There’s a generator below for the config and the release workflow, so you can skip the copying.

Definition

What Changesets is

Changesets is a tool for versioning and publishing npm packages, built with monorepos in mind, where each change comes with a small markdown file saying which packages it touches and whether each one gets a major, minor or patch bump. You write that file, usually in the same pull request as the code. At release time changeset version reads every pending file, bumps the versions, writes each package’s CHANGELOG.md and deletes the files it used. changeset publish puts the result on npm. Next to the rest of this cluster it’s the odd one out, because it never reads commit messages. Conventional commits are optional here. The current major is v3 (@changesets/cli 3.0.3). The word also means a group of commits in version control, and a unit of database migration in Liquibase. This page is about the npm tool.

Free tool

Generate the config

Set your release branch and whether you publish to npm. You get the .changeset/config.json that init writes, and a workflow that runs changesets/action@v2 on every push to that branch. The switch at the top shows the same repo set up for release-please or semantic-release, if you’re still deciding.

Release config generator

  1. .changeset/config.json

    What `changeset init` writes, with your branch. "access": "public" is needed to publish a scoped package publicly.

    {
      "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json",
      "baseBranch": "main",
      "access": "restricted",
      "format": "auto",
      "changelog": "@changesets/cli/changelog",
      "commit": false,
      "ignore": [],
      "fixed": [],
      "linked": [],
      "updateInternalDependencies": "patch"
    }
    
  2. .github/workflows/release.yml

    Opens a "Version Packages" PR while changesets are pending. Merging it lands the version bumps and changelog.

    name: Release
    
    on:
      push:
        branches:
          - main
    
    permissions: {}
    
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
    
    jobs:
      release:
        runs-on: ubuntu-latest
        permissions:
          contents: write # to commit version changes and create releases
          pull-requests: write # to create the version PR
        steps:
          - name: Check out repo
            uses: actions/checkout@v7
          - name: Set up Node.js
            uses: actions/setup-node@v7
            with:
              node-version: 24
          - name: Install dependencies
            run: npm install
          - name: Open or update the version PR
            uses: changesets/action@v2
    

Checked on 2026-09-23: each tool loaded its generated config and cut a test release from it, and every file this form can produce parses. If an action has shipped a newer major since, bump the number after the @.

Minimum working setup

Setup

npm install -D @changesets/cli
npx @changesets/cli init

In v3, init is interactive. It asked me four things: use the GitHub integration for changelogs, commit changesets and version bumps automatically, publish public or private by default, and which base branch. Taking every default wrote this, plus a README in the same folder:

{
  "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json",
  "baseBranch": "main",
  "access": "restricted",
  "format": "auto",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "ignore": [],
  "fixed": [],
  "linked": [],
  "updateInternalDependencies": "patch"
}

Saying yes to the GitHub question asks for the repo and swaps the changelog line for this one:

"changelog": [
  "@changesets/changelog-github",
  {
    "repo": "your-org/your-repo"
  }
],

Then, for each change worth releasing, add a changeset. The interactive picker splits your packages into “changed” and “unchanged”, which helps in a big repo. The CLI also takes flags, which is what you want in a script:

# interactive: pick packages, pick bump types, write a summary
npx @changesets/cli

# or all in one line
npx @changesets/cli --minor frog-core -m "Add frog search"

Either way you get a file with a generated three-word name in .changeset/. Mine was dry-walls-shout.md:

---
"frog-core": minor
---

Add frog search

The front matter is the bump and the rest is the changelog entry, word for word, so write it for the person reading the changelog. Commit it. With commit: false, nothing gets committed for you. Here’s changeset status --verbose once it was in, then what changeset version wrote to packages/frog-core/CHANGELOG.md as it moved the package from 1.0.0 to 1.1.0:

Packages to be bumped:
- minor
  - frog-core -> 1.1.0
    - .changeset/dry-walls-shout.md
# frog-core

## 1.1.0

### Minor Changes

- b5b2561: Add frog search

The short hash is the commit that added the changeset, not the one with the code in it. If you care how that file is laid out, the changelog format guide covers the conventions.

The monorepo question

What happens to dependent packages

This is the question everyone gets stuck on. My test workspace had two packages at 1.0.0, and frog-cli depended on frog-core at ^1.0.0. I ran changeset version five times with different changesets pending:

Changesets pendingfrog-corefrog-clifrog-cli’s range for frog-core
frog-core: minor1.1.01.0.0, not released^1.0.0, untouched
frog-core: major2.0.01.0.1, automatic patch^2.0.0
frog-core: minor, frog-cli: patch1.1.01.0.1^1.1.0
both patch, updateInternalDependencies "patch"1.0.11.0.1^1.0.1
both patch, updateInternalDependencies "minor"1.0.11.0.1^1.0.0, untouched

Two rules explain the whole table. A package that isn’t in the release gets pulled in only when the new version falls outside its range. 1.1.0 still satisfies ^1.0.0, so the first row leaves frog-cli alone. 2.0.0 doesn’t, so the second row gives it a patch release with nothing but this in its changelog:

# frog-cli

## 1.0.1

### Patch Changes

- Updated dependencies [f529687]
  - frog-core@2.0.0

The CLI says so when you add the changeset: “All packages that depend on these whose required versions will be incompatible will also be patch bumped when this changeset is applied.”

The second rule is updateInternalDependencies, and its name oversells it. The config schema calls it “the minimum bump type to trigger automatic update of internal dependencies that are part of the same release.” Part of the same release is the bit that matters. It only rewrites ranges in packages that are already being released. At the default, "patch", even a patch bump rewrites the range. Set it to "minor" and the last row happens: both packages go to 1.0.1 and the range stays at ^1.0.0.

Enforcement

Making sure PRs have a changeset

The weak spot is obvious. A human has to remember a file. The docs give you two options and prefer the gentle one. The changeset bot is a GitHub App that comments on each PR saying whether it has a changeset, with a link that opens a new changeset file, name already filled in, so a maintainer can write one in the browser and merge without waiting. v2 of the Action also ships pr-status and pr-comment sub-actions that do the same from your own workflow.

The strict option is a CI step running npx @changesets/cli status --since main. I checked the exit codes. Files inside a package changed and no changeset: exit 1. Only files outside the packages changed: exit 0. A changeset present: exit 0. One catch: it only counts committed changesets. With the file sitting uncommitted in the working tree it still failed.

For a change that shouldn’t release anything, like tests or CI config inside a package folder, changeset --empty writes a changeset with nothing in it. It passes the check and version just deletes it:

---

---

The docs themselves don’t recommend blocking, since not every change needs a release. I’d still rather have the red X than find out from a missing changelog entry.

Automation

The GitHub Action

changesets/action@v2 goes with Changesets v3. v1 is the maintenance line for v2, so don’t mix them. On each push to your branch it checks for pending changesets. If there are some, it runs changeset version and opens or updates a PR titled “Version Packages”. Merging that PR consumes the changesets, so with a publish-script set, the next run publishes instead.

  • The job needs contents: write and pull-requests: write, plus id-token: write for npm trusted publishing.
  • Turn on Settings, Actions, General, “Allow GitHub Actions to create and approve pull requests”. Without it the Action can’t open the Version Packages PR.
  • PRs opened with the default token don’t trigger your other workflows, so CI won’t run on the Version Packages PR. The fix is a personal token or a GitHub App token passed through the github-token input. Setting a GITHUB_TOKEN env var does not configure the Action.
  • The docs recommend npm trusted publishing and say staged publishing doesn’t work with Changesets yet. For trusted publishing they split the single Action into select-mode, version, pack and publish jobs so only the publish job gets id-token: write.

The generator above writes the single-Action workflow the docs offer as the simplified option for private repos with trusted contributors, and that’s where it belongs. For a public package, read the trusted publishing section of the automating guide first.

Troubleshooting

Errors people hit

All but the last came out of my own terminal.

  • No versionable packages found, followed by a hint to check that your package.json files have a version field. Mine did. The real cause was an npm workspace with no package-lock.json. Changesets finds workspaces through @manypkg, which only treats an npm root as a monorepo when the lockfile is next to it. Without it the only package it saw was my private root. npm install, commit the lockfile, done.
  • Some packages have been changed but no changesets were found. Run changeset add to resolve this error. That’s status --since doing its job. Add a changeset, or an empty one, and commit it.
  • Failed to find where HEAD diverged from "main". Does "main" exist and it’s synced with remote? I got this two ways. A shallow clone, depth 1, failed even after I fetched main, and git fetch --unshallow fixed it. A full clone with no local main branch failed on --since=main and passed on --since=origin/main. In CI, that means full history and a ref that exists.
  • could not parse changeset - invalid version type "minr" for package "frog-core". Valid version types are: major, minor, patch, none. A typo in the front matter. Its sibling is Found changeset bad-name for package frog-ui which is not in the workspace, from a changeset naming a package that doesn’t exist, say after a rename. Both stop status and version with a stack trace. Fix the file by hand.
  • GitHub Actions is not permitted to create or approve pull requests, or remote: Permission to xxx.git denied to github-actions[bot]. The docs list these for the repo setting above being off. Turn it on.

One more that isn’t really an error: changeset version with nothing pending prints No unreleased changesets found. and exits 1, so don’t call it unconditionally in a script.

Choosing

When it’s the right tool

Changesets wins when you have several npm packages in one repo that depend on each other, because the dependent bumps in the table above are the part people get wrong by hand. It also wins when you want contributors to describe their own change in words meant for users. A changeset summary is a changelog line someone actually wrote. A commit subject is whatever they typed at 6pm. And nobody has to police commit messages.

It loses on a single package where your team already writes conventional commits. There the extra file is pure overhead, and release-please gives you the same release PR flow straight from the commit log. If you want no PR at all and a release on every qualifying push, semantic-release does that. It’s also built around package.json and npm. The docs have a “Beyond npm” guide, but for a Go or Python project the release types in release-please fit more naturally. And the file can be forgotten, which is why the bot exists. If you only want a changelog and no versioning, the changelog generators comparison is the shorter path.

Honesty

What this doesn’t fix

Changesets gets you correct versions across a monorepo and a changelog that people wrote on purpose. Then the Version Packages PR merges, npm has the new version, and nobody outside the repo hears about it. The changelog sits there for the people who already use the thing and went looking.

That gap is the one I built Merge & Tell for. It reads each merged pull request, the diff and not just the title, and drafts a post for each network. I read every one before it goes out. The updates page is that running on this product.

Version Packages merged. Now tell someone.

You were going to merge the PR anyway.

Changesets guide: setup, monorepo bumps and errors