changesette
A version and changelog manager using the same changeset file format as changesets and shipped as a single dependency-free Rust binary. The name is changeset + the diminutive suffix -ette (as in diskette).
changesette reads changeset files, bumps the version in each named package's package.json, and generates its CHANGELOG.md. It works on single-package repositories and on npm / yarn / pnpm workspaces. It bumps only the packages named in changesets, does no dependency management (Workspaces covers what happens instead), and never touches lockfiles; regenerating lockfiles such as package-lock.json belongs to the package-manager layer.
changesette performs no git operations and no network access; commits, pull requests, tags, and releases belong to your workflows. The CLI feeds those workflows structured data: a machine-readable release plan (version --output), the workspace package list (get-packages), and per-version changelog sections (get-changelog-entry). The example workflows build the whole release loop from these outputs — no changesets-specific action or bot required.
Install
GitHub Actions (verifies the build provenance of the downloaded archive; GitHub-hosted runners are assumed):
uses: iorate/changesette/setup@v3
Shell script (macOS / Linux):
|
PowerShell (Windows):
powershell -ExecutionPolicy Bypass -c "irm https://github.com/iorate/changesette/releases/latest/download/changesette-installer.ps1 | iex"
Homebrew:
npm:
Cargo (requires Rust 1.85+):
Example workflows
Single package (npm)
On every push to main, maintains a Version PR that applies the pending changesets; merging it publishes the package to the npm registry and creates a GitHub Release (and its tag) with the changelog section as the notes. A version whose section is missing from the changelog (for example one released before adopting changesette) gets no GitHub Release. Replace my-package with the name declared in your package.json.
name: Version
on:
push:
branches:
- main
concurrency: version
jobs:
version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write # npm trusted publishing (OIDC)
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: iorate/changesette/setup@v3
- id: version
run: |
plan="$(changesette version --allow-no-changesets --output -)"
if release="$(jq -e '[.releases[] | select(.type != "none")][0]' <<< "$plan")"; then
version="$(jq -re '.newVersion' <<< "$release")"
echo "title=Release v$version" >> "$GITHUB_OUTPUT"
delim="$(openssl rand -hex 16)"
{
echo "body<<$delim"
jq -re '.changelogEntry' <<< "$release"
echo "$delim"
} >> "$GITHUB_OUTPUT"
npm install --package-lock-only
else
echo "title=Consume changesets" >> "$GITHUB_OUTPUT"
fi
- id: pr
uses: peter-evans/create-pull-request@v8
with:
branch: changesette/release
commit-message: ${{ steps.version.outputs.title }}
title: ${{ steps.version.outputs.title }}
body: ${{ steps.version.outputs.body }}
delete-branch: true
- if: steps.pr.outputs.pull-request-number == ''
run: |
version="$(jq -re .version package.json)"
if ! npm view "my-package@$version" version > /dev/null 2>&1; then
npm publish
fi
if ! gh release view "v$version" > /dev/null 2>&1; then
if notes="$(changesette get-changelog-entry my-package "$version")"; then
gh release create "v$version" \
--target "$GITHUB_SHA" \
--notes "$notes"
fi
fi
env:
GH_TOKEN: ${{ github.token }}
Workspace (pnpm)
On every push to main, maintains a Version PR that applies the pending changesets; merging it publishes the bumped packages to the npm registry with pnpm and creates a GitHub Release (and its tag, <name>@<version>) per package with the changelog section as the notes. pnpm publish -r publishes every workspace package whose version is not on the registry yet and skips the rest, so no per-package bookkeeping is needed; insert a build step before it if your packages need one. A package whose changelog has no section for its current version (for example a private package never named in a changeset) gets no GitHub Release. With npm instead of pnpm, there is no equivalent of pnpm publish -r; iterate over changesette get-packages and publish each package whose version is not on the registry yet.
name: Version
on:
push:
branches:
- main
concurrency: version
jobs:
version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write # npm trusted publishing (OIDC)
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: pnpm/action-setup@v6
- uses: iorate/changesette/setup@v3
- id: version
run: |
plan="$(changesette version --allow-no-changesets --output -)"
if jq -e 'any(.releases[]; .type != "none")' <<< "$plan" > /dev/null; then
echo "title=Version packages" >> "$GITHUB_OUTPUT"
delim="$(openssl rand -hex 16)"
{
echo "body<<$delim"
jq -r '[.releases[] | select(.type != "none") | "## \(.name)@\(.newVersion)\n\n\(.changelogEntry)"] | join("\n\n")' <<< "$plan"
echo "$delim"
} >> "$GITHUB_OUTPUT"
pnpm install --lockfile-only
else
echo "title=Consume changesets" >> "$GITHUB_OUTPUT"
fi
- id: pr
uses: peter-evans/create-pull-request@v8
with:
branch: changesette/release
commit-message: ${{ steps.version.outputs.title }}
title: ${{ steps.version.outputs.title }}
body: ${{ steps.version.outputs.body }}
delete-branch: true
- if: steps.pr.outputs.pull-request-number == ''
run: |
pnpm install --frozen-lockfile
pnpm publish -r
packages="$(changesette get-packages)"
jq -c '.[]' <<< "$packages" | while read -r package; do
name="$(jq -re .name <<< "$package")"
version="$(jq -re .version <<< "$package")"
if ! gh release view "$name@$version" > /dev/null 2>&1; then
if notes="$(changesette get-changelog-entry "$name" "$version")"; then
gh release create "$name@$version" \
--target "$GITHUB_SHA" \
--notes "$notes"
fi
fi
done
env:
GH_TOKEN: ${{ github.token }}
CLI
changesette init
Creates the .changeset/ directory with a README.md. Does nothing if the directory already exists.
changesette [add] [--empty] [--message <text>] [--major <pkgs>] [--minor <pkgs>] [--patch <pkgs>]
Creates a changeset file in .changeset/. --empty creates a changeset that names no packages and conflicts with the bump flags; --message (short form -m) sets the summary; --major, --minor, and --patch each take a comma-separated list of package names and may be repeated. When run in a terminal, missing inputs are prompted for interactively: the affected packages and their bump types when no bump flag is given, and the summary when --message is not given (submitting an empty summary opens your editor for a multi-line one).
changesette version [--ignore <pkgs>] [--allow-no-changesets] [--output <file>]
Applies all pending changesets: bumps each named package's package.json, inserts the new section into its CHANGELOG.md, and deletes the consumed changesets. Each package receives the widest bump across the changesets naming it. Packages named only with the none type keep their version and changelog, but their changesets are still deleted, as are empty changesets. With zero changesets, nothing changes and the command fails; --allow-no-changesets (short form -a) makes it succeed instead, and exiting pre-release mode always succeeds. Lockfiles are not updated; if you use npm, run npm install --package-lock-only afterwards.
In pre-release mode, version bumps to -<tag>.<n> prereleases and moves the consumed changesets to .changeset/pre/ instead of deleting them; see Pre-release mode.
--ignore takes a comma-separated list of package names and may be repeated. Each name must be a workspace member's package name. Changesets naming an ignored package are skipped: they are excluded from the release plan and left in place for a later run. A changeset naming both an ignored and a not-ignored package is an error.
--output (short form -o) suppresses the report and writes the release plan to the given file (- for stdout) as pretty-printed JSON, extending the changesets ReleasePlan type with changelogEntry (with --allow-no-changesets, an empty plan when there are zero changesets):
releases lists every named package. changelogEntry is the body of the package's new changelog section, without the ## <version> heading; a "none"-type release has an unchanged version and no changelogEntry. In pre-release mode, a top-level "preState" object ({ "mode", "tag" }) is included, and the ids of the changesets in .changeset/pre/ carry a pre/ prefix.
changesette status [--verbose] [--output <file>]
Prints the packages that version would bump, without changing any file. --verbose (short form -v) adds each package's new version and the changeset files naming it. --output (short form -o) writes the release plan to the given file (- for stdout) instead of printing the list — the same JSON version --output writes. Packages named only with the none type appear in the JSON but not in the list.
changesette pre enter <tag>
Enters pre-release mode by writing .changeset/pre.json with the given tag (the beta of 1.1.0-beta.0), creating .changeset/ if needed. The tag must be a valid semver pre-release identifier sequence, such as beta, rc-1, or beta.2. It is an error to already be in pre mode; a pre.json left in the exited state is rewritten in place.
changesette pre exit
Leaves pre-release mode by flipping .changeset/pre.json to the exited state, so that the next version bumps to final versions and deletes the file. It is an error to have no pre.json; running it twice is harmless.
changesette get-packages
Prints the workspace packages to stdout as a single-line JSON array in package name order. Each entry has the package's name, its version, and its dir relative to the workspace root ("." when the package is the workspace root itself):
changesette get-changelog-entry <package> <version>
Prints the body of the ## <version> section of the named package's CHANGELOG.md — the text below that heading, without the heading itself.
Pre-release mode
Pre-release mode publishes 1.3.0-beta.0, 1.3.0-beta.1, … from the pending changesets before releasing 1.3.0. pre enter and pre exit maintain .changeset/pre.json, in the same format changesets uses:
While in pre mode, version moves the changesets it consumes to .changeset/pre/ instead of deleting them. Once pre mode is exited, the next version plans the parked changesets together with the new ones into the final version and deletes both the consumed changesets and pre.json. A package left on a prerelease version that no changeset names is given a patch bump too, which amounts to dropping its -<tag>.<n> suffix; --ignore exempts it.
Choosing the npm dist-tag is up to you, as changesette never publishes: pass --tag <tag> while pre-releasing so that latest keeps pointing at the stable version.
Workspaces
changesette works on npm / yarn / pnpm workspaces, and its changeset files are format-compatible with changesets — but version deliberately does not behave like changeset version in a workspace. The dependency management changesets performs is two separate jobs, and changesette does neither: internal dependency ranges are never rewritten, and dependents of a bumped package are never bumped (so no "Updated dependencies" changelog entries either). Only the packages explicitly named in changesets are bumped.
Ranges are a mechanical job, and the workspace: protocol of yarn and pnpm makes it the package manager's: in development a workspace: dependency always resolves to the local copy, and at publish the range is derived from the dependency's current version (workspace:^ becomes a caret range, workspace:* an exact pin, and so on), so published ranges always reflect the versions the dependent was actually built against. Plain npm workspaces work too, but literal ranges like ^1.2.0 are then yours to maintain: rewrite them when the dependency moves to a new major (otherwise npm stops linking the local copy), and raise them when the dependent starts relying on newer behavior.
Dependent releases are a judgment, not bookkeeping. Already-published dependents keep working after an internal dependency's major release: their published ranges still resolve to the old, compatible versions. So release a dependent when it has changes of its own; the one other reason is a consumer who cannot run two copies of the dependency side by side (a peer dependency conflict, a shared singleton) and so needs a published range that accepts the new major. Either way, name the dependent in a changeset like any other change.
Differences from changesets
changesette shares the changeset file format with changesets, but is deliberately much smaller. Coming from changesets, expect the following:
- No dependency management: dependents of a bumped package are never bumped, and dependency ranges are never rewritten (see Workspaces).
- No configuration:
.changeset/config.jsonis not read, and there is nothing to configure (nofixed/linked;ignoreexists only as theversion --ignoreflag). - No changed-package detection:
adddoes not inspect git to suggest packages, andstatushas no--since. - No changelog decoration: entries are the plain changeset summaries, without auto-generated PR / commit / author links, and there are no changelog plugins.
- No drop-in command compatibility: the implemented commands follow
changeset's flags and exit codes, but coverage is partial and terminal output differs; the changeset files are fully interchangeable, and the release plan JSON written by--outputextends the changesetsReleasePlantype withchangelogEntry.