Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

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:

When to Use This Reach for changesets when any of these are true: When to Use This Reach for changesets when any of these are true:
When to Use This — the core idea of this section at a glance.
  • 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.

Changesets release flow Adding changesets accumulates intent files, the version step consumes them and bumps packages, then publish ships to the registry. add changeset per pull request version consume files bump + changelog release PR review + merge publish to registry Intent files become a reviewable release
Each pull request adds a changeset; the version step batches them into a release pull request, which on merge publishes.

Setup Steps

  1. Install the CLI as a dev dependency at the repo root:
    npm install --save-dev @changesets/cli
  2. Initialize the config and .changeset/ directory:
    npx changeset init
  3. 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; updateInternalDependencies controls how dependents are bumped when a workspace package changes; ignore excludes private apps that should never publish.
  4. Add release scripts to the root package.json:
    {
      "scripts": {
        "changeset": "changeset",
        "version-packages": "changeset version",
        "release": "changeset publish"
      }
    }
    changeset version consumes intent files and writes bumps; changeset publish builds and publishes anything whose version is ahead of the registry, creating Git tags per package.
  5. Author a changeset whenever you make a user-facing change:
    npx changeset
    # interactive: pick packages, pick bump level, write a summary
    Commit the generated .changeset/<name>.md alongside your code change.
Setup Steps access: "public" is required for scoped packages; updateInternalDependencies controls how dependents are bumped when a w Setup Steps access: "public" is required for scoped packages; updateInternalDependencies controls how dependents are bumped when a workspace package changes; ignore exclude
Setup Steps — the core idea of this section at a glance.

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.

CI Integration The official changesets GitHub Action handles both halves automatically: on a normal push it opens or updates a "Version 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 me
CI Integration — the core idea of this section at a glance.
# .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:

Snapshot Releases To publish a throwaway preview from a feature branch without touching the changelog or latest: Snapshot Releases To publish a throwaway preview from a feature branch without touching the changelog or latest:
Snapshot Releases — the core idea of this section at a glance.
# 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

Validation Commands Validation Commands in production JavaScript package workflows. Validation Commands Validation Commands in production JavaScript package workflows.
Validation Commands — the core idea of this section at a glance.
# 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 ignore so a private workspace package never gets versioned or published by accident.
  • Set fetch-depth: 0 on checkout; changesets needs full history to diff against baseBranch.
  • Use a scoped, short-lived NPM_TOKEN and run the publish only from main behind a concurrency guard.
Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows. Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows.
Prevention & CI Guardrails — the core idea of this section at a glance.
  • Require a changeset on any PR that modifies a publishable package, enforced by changeset status.
  • Use the changesets GitHub action to open and manage the release PR automatically.
  • Add an empty changeset for deliberately non-releasing changes.
  • Configure updateInternalDependencies so a change propagates to dependent packages.

The release PR workflow

The distinctive part of Changesets automation is the release pull request, which separates accumulating changes from cutting a release. As changes merge to the main branch with their changesets, the changesets action maintains an open 'Version Packages' pull request that shows exactly what the next release will contain — the version bumps and the assembled changelog entries. Merging that PR applies the version changes and triggers the publish, so cutting a release is a single, reviewable action rather than a command run from a laptop.

Release PR Changesets accumulate; merging the PR cuts the release. merge changes with changesets release PR updates versions + changelog merge to publish reviewable release
A standing release PR shows the next version and changelog before you publish.

This workflow gives a team control over release timing while keeping the mechanics automatic. Changes can accumulate on main for days, each adding to the pending release PR, and the release goes out when someone merges it — which might be on a schedule, at a milestone, or whenever enough has accumulated. The PR itself is the review artifact: it shows every package's next version and changelog before anything is published, so a surprising bump or a missing entry is caught before the release rather than after. Between the per-change changesets and the aggregating release PR, the process makes both what is released and when it is released explicit and reviewable, which is the core appeal of the intent-driven model for a monorepo.

Versioning internal dependencies correctly

In a monorepo, a change to one package often requires bumping the packages that depend on it, and Changesets handles this through its internal-dependency configuration. When you add a changeset for @acme/core, Changesets can automatically bump the dependents that reference it so a consumer of @acme/ui receives a version that pulls in the fixed @acme/core. The updateInternalDependencies setting controls whether this propagation happens on patch-level changes or only on minor-and-above.

Internal bumps A changed package bumps its dependents per threshold. changeset for core the change bump dependents per threshold consumers get fix no stale reference
Configure the internal-dependency threshold so a change propagates to its consumers.

Getting this configuration right prevents a subtle release gap where a dependency changes but its dependents are not republished, leaving consumers on a version that references a stale internal package. Aligning the threshold with how tightly your packages couple — patch-level propagation for tightly-linked packages, minor-and-above for more independent ones — ensures a change reaches the consumers whose behavior it affects. This is the monorepo-specific complement to the basic requirement that each changed package have a changeset: not only must the changed package be released, but the dependents whose output the change affects must be bumped too, which is exactly what the automated internal-dependency versioning provides.

Enforcing changesets in CI

The one failure mode of the Changesets model — a change merged without a changeset, producing no release — is prevented by enforcing the requirement in CI. The changeset status --since=<base> command exits non-zero when a publishable package changed without an accompanying changeset, so making it a required check turns a silent no-op release into a red build on the pull request that introduced the gap. The contributor is prompted to run pnpm changeset and describe the bump before merge, so the intent is captured with the change rather than remembered later.

Changeset CI gate A status check fails a PR missing its changeset. changeset status compare to base missing intent red build empty for non-release explicit no-op
A required status check makes the intent-driven model self-enforcing.

Deliberately non-releasing changes are handled by adding an empty changeset, which explicitly records 'no release needed' rather than leaving the intent ambiguous — a refactor or a docs edit passes the check with an empty changeset. This makes the intent-driven model self-enforcing: every change either carries a changeset describing its release impact or an empty one declaring it has none, and the CI gate ensures one of the two is always present. Without the check, the model relies on everyone remembering the extra step, which is exactly the discipline that erodes under deadline pressure; with it, a forgotten changeset cannot reach the main branch, so the release is always complete.

Prerelease and snapshot releases with Changesets

Changesets supports prerelease and snapshot modes for the cases where the standard release PR flow is not what you want. Entering prerelease mode with changeset pre enter next makes subsequent changeset version runs produce x.y.z-next.N versions, so you can publish a release candidate line under a next tag and validate it before exiting prerelease mode and cutting the stable release. This gives the same staged-rollout capability that dist-tags provide, driven by the accumulated changesets.

Prerelease modes Prerelease versus snapshot releases. Mode Produces Use pre enter x.y.z-next.N staged rollout snapshot 0.0.0-pr-N test a PR
Prerelease mode stages a rollout; snapshots publish throwaway PR versions.

Snapshot releases serve a different need: publishing an ephemeral version from a pull request for testing, without affecting the normal versioning. changeset version --snapshot pr-123 produces a throwaway version like 0.0.0-pr-123-20240101 that a reviewer can install to test the PR's changes in a real consumer, and which never becomes part of the stable release history. Between prerelease mode for staged rollouts and snapshot releases for PR testing, Changesets covers the release scenarios beyond the standard merge-the-release-PR flow, so a team can validate risky or in-progress changes with real installs while keeping the stable release process clean and intent-driven.

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.

How does the Changesets release PR work?

As changes merge with their changesets, the changesets action maintains an open 'Version Packages' PR showing the next release's version bumps and changelog. Merging that PR applies the versions and publishes, so cutting a release is a single reviewable action and you control the timing.

Why didn't a dependent package get released when its dependency changed?

Changesets bumps internal dependents based on the updateInternalDependencies threshold. If it is set to minor-and-above and you made a patch change, dependents are not bumped. Lower the threshold for tightly-coupled packages so a change propagates to the consumers that need it.

How do I make sure every change has a changeset?

Add a required CI check running changeset status --since=origin/main, which fails a PR that changes a publishable package without a changeset. For a deliberately non-releasing change, add an empty changeset so the intent — no release — is explicit and the check passes.

How do I publish a prerelease or a test version with Changesets?

Use changeset pre enter next for a prerelease line that produces x.y.z-next.N versions under a next tag, or changeset version --snapshot pr-123 for a throwaway version to test a PR in a real consumer. Both keep the stable release history clean.

Does Changesets work outside of GitHub?

Yes — the core CLI (changeset, changeset version, changeset publish) is platform-agnostic and runs in any CI. The changesets GitHub action automates the release PR specifically, but on other platforms you can script the version-and-publish steps directly.

Related

Semantic Versioning and Release Automation