Automating Changesets
Changesets provides first-class support to automate the release process on GitHub. Generally, this can be broken into two major decisions:
- How do I ensure pull requests have changesets?
- How do I run the version and publish commands?
Toolings
This guide uses these tools to automate the release process. Check out their documentation for specific information on setting up and supported APIs.
How do I ensure pull requests have changesets?
Changesets are committed to files, so a diligent reviewer can always technically tell if a changeset is absent and request one to be added. As humans though, a file not being there is easy to miss.
We recommend adding some way to detect the presence or absence of changesets on a pull request so you don't have to, as well as highlight it directly to the pull request author.
This has two main approaches: non-blocking and blocking.
Non-blocking
In this approach, a pull request may be merged if no changeset is present, and a missing changeset does not cause a failure in CI. The Changesets GitHub Bot is the easiest way to prompt for changesets without making them blocking.
Alternatively, you can use the Changesets GitHub Action to write a custom workflow that does the check.
DO NOT RUN UNTRUSTED CODE
Do not run untrusted code when using the pull_request_target event.
pull_request_target can be useful to support PRs from forks, however it enables write permissions by default which can be a security risk if untrusted code is executed and the permissions aren't scoped down.
The example below only checks out and reads code, and does not execute any code from the fork.
You can also use the pull_request event if you prefer to lock permissions down and not run for PRs from forks. Make sure to add an if check to prevent the action from failing in fork PRs:
jobs:
pr-status:
if: github.event.pull_request.head.repo.full_name == github.repository
# ...name: Comment Changesets PR Status
on:
pull_request_target:
permissions: {} # recommended: reset permissions
# recommended: avoid concurrent runs for the same pr
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
jobs:
pr-status:
runs-on: ubuntu-slim
permissions:
contents: read # to check out repo (actions/checkout)
outputs:
comment-body: ${{ steps.pr-status.outputs.comment-body }}
steps:
- name: Check out repo
uses: actions/checkout@v7
- name: Generate PR status
id: pr-status
uses: changesets/action/pr-status@v2
pr-comment:
needs: pr-status
runs-on: ubuntu-slim
permissions:
pull-requests: write # to create and update comments on PRs (changesets/action/pr-comment)
steps:
- name: Comment on PR
uses: changesets/action/pr-comment@v2
with:
body: ${{ needs.pr-status.outputs.comment-body }}Both approaches comment on PRs of whether changesets are present and gives you link to add your own changeset as a maintainer to smooth over merging pull requests without waiting for the contributor to add a changeset.
Blocking
As not every change requires a release, we do not recommend blocking contributions in the absence of a changeset. However, if you prefer a consistent process that always requires a changeset, you can add a step in your CI setup that runs:
$ pnpm changeset status --since main$ npx @changesets/cli status --since main$ yarn changeset status --since mainThis will exit with code 1 if there are changed packages but no new changesets since main. It will not fail if there are no changed packages.
If you want to merge a change without doing any releases (such as when you only change tests or build tools), you can run changeset --empty to add a special changeset that does not release anything.
How do I run the version and publish commands?
You can set up the Changesets GitHub Action to automate this process. The workflow varies depending if you publish with Trusted Publishing, with npm tokens, or without publishing at all.
Generally, the setup we recommend works like this:
Prerequisites
As the Changesets GitHub Action creates pull requests for versioning, ensure in your repository settings, in Actions > General, the Allow GitHub Actions to create and approve pull requests option is enabled.
If this is not enabled, you may encounter errors such as:
remote: Permission to xxx.git denied to github-actions[bot]GitHub Actions is not permitted to create or approve pull requests
Trusted Publishing
It is recommended by npm to use Trusted Publishing, or Staged Publishing, or both, to securely publish packages from CI.
At the moment, Staged Publishing does not work with Changesets, so you should use Trusted Publishing instead for now. Check out its docs for more information to set it up.
Also, in contrary to npm's workflow recommendation, make sure the id-token: write is only set on the job that needs to publish. As such, consider splitting the build, test, publish flows etc into separate jobs. Here's an example setup:
name: Publish
on:
push:
branches:
- main
# recommended: reset permissions and explicitly specify for each job
permissions: {}
# recommended: avoid concurrent runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
select-mode:
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.select-mode.outputs.mode }}
publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }}
permissions:
contents: read # to check out repo (actions/checkout)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
- name: Install dependencies
run: npm install
- name: Select Changesets mode
id: select-mode
uses: changesets/action/select-mode@v2
version:
if: needs.select-mode.outputs.mode == 'version'
needs: select-mode
runs-on: ubuntu-latest
outputs:
version-dir-artifact-id: ${{ steps.version.outputs.version-dir-artifact-id }}
permissions:
contents: write # to check out repo (actions/checkout) and commit version changes (changesets/action/version)
pull-requests: write # to create pull request (changesets/action/version)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Version packages
id: version
uses: changesets/action/version@v2
pack:
if: needs.select-mode.outputs.mode == 'publish'
needs: select-mode
runs-on: ubuntu-latest
outputs:
pack-dir-artifact-id: ${{ steps.pack.outputs.pack-dir-artifact-id }}
permissions:
contents: read # to check out repo (actions/checkout)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Build packages
run: npm build
- name: Pack packages
id: pack
uses: changesets/action/pack@v2
with:
publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }}
publish:
needs: pack
runs-on: ubuntu-latest
permissions:
contents: write # to check out repo (actions/checkout) and to create releases (changesets/action/publish)
id-token: write # for trusted publishing (changesets/action/publish)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Publish packages
uses: changesets/action/publish@v2
with:
pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }}Token-based Publishing
WARNING
Token-based publishing (with Granular Access Tokens) is no longer recommended, with many restrictions that make it difficult to use in CI workflows. For example:
- They expire after a maximum of 90 days, which requires periodic manual token rotation.
- 2FA-bypass tokens are being deprecated and will soon be not allowed to publish packages with 2FA enabled.
However, if you're using a different npm-compatible registry that does not support Trusted Publishing or Staged Publishing, you may still opt for token-based publishing.
You'll need an npm token with "Bypass two-factor authentication" checked to prevent npm requesting 2FA in CI. Add this token as a secret in your GitHub repo with the name NPM_TOKEN so it can be used in the workflow below.
In most cases, you can use actions/setup-node to set up token-based authentication:
name: Publish
on:
push:
branches:
- main
# recommended: reset permissions and explicitly specify for each job
permissions: {}
# recommended: avoid concurrent runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
select-mode:
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.select-mode.outputs.mode }}
publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }}
permissions:
contents: read # to check out repo (actions/checkout)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
- name: Install dependencies
run: npm install
- name: Select Changesets mode
id: select-mode
uses: changesets/action/select-mode@v2
version:
if: needs.select-mode.outputs.mode == 'version'
needs: select-mode
runs-on: ubuntu-latest
outputs:
version-dir-artifact-id: ${{ steps.version.outputs.version-dir-artifact-id }}
permissions:
contents: write # to check out repo (actions/checkout) and commit version changes (changesets/action/version)
pull-requests: write # to create pull request (changesets/action/version)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Version packages
id: version
uses: changesets/action/version@v2
publish:
if: needs.select-mode.outputs.mode == 'publish'
needs: select-mode
runs-on: ubuntu-latest
permissions:
contents: write # to check out repo (actions/checkout) and to create releases (changesets/action/publish)
steps:
- name: Check out repo
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org/ # set this option to set up npm authentication
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Build packages
run: npm build
- name: Publish packages
uses: changesets/action/publish@v2
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # pass the token hereAlternative simplified workflow for private repos with trusted contributors
name: Publish
on:
push:
branches:
- main
# recommended: reset permissions and explicitly specify for each job
permissions: {}
# recommended: avoid concurrent runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # to check out repo (actions/checkout) and to create releases (changesets/action)
pull-requests: write # to create pull request (changesets/action)
steps:
- name: Check out repo
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org/ # set this option to set up npm authentication
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Build packages
run: npm build
- name: Version or publish packages
uses: changesets/action@v2
with:
publish-script: npm @changesets/cli publish # set this option to any command that executes `changeset publish` to publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # pass the token hereTIP
Pass registry-url: https://npm.pkg.github.com/ and NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} to publish to the GitHub Package Registry instead of npm.
Publish Git Tags Only
In case you have a separate workflow to publish your packages, e.g. workflows triggered on git tags creation, you can use Changesets to only create git tags on publish.
This requires making your packages' package.json "private": true and setting privatePackages to true. See the Beyond npm guide for more information.
You can use the following workflow, similar to Token-based Publishing above, but without the registry-url and NODE_AUTH_TOKEN set for npm authentication:
name: Publish
on:
push:
branches:
- main
# recommended: reset permissions and explicitly specify for each job
permissions: {}
# recommended: avoid concurrent runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
select-mode:
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.select-mode.outputs.mode }}
publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }}
permissions:
contents: read # to check out repo (actions/checkout)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
- name: Install dependencies
run: npm install
- name: Select Changesets mode
id: select-mode
uses: changesets/action/select-mode@v2
version:
if: needs.select-mode.outputs.mode == 'version'
needs: select-mode
runs-on: ubuntu-latest
outputs:
version-dir-artifact-id: ${{ steps.version.outputs.version-dir-artifact-id }}
permissions:
contents: write # to check out repo (actions/checkout) and commit version changes (changesets/action/version)
pull-requests: write # to create pull request (changesets/action/version)
steps:
- name: Check out repo
uses: actions/checkout@v7
with:
persist-credentials: false # recommended: do not persist git credentials on disk
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Version packages
id: version
uses: changesets/action/version@v2
publish:
if: needs.select-mode.outputs.mode == 'publish'
needs: select-mode
runs-on: ubuntu-latest
permissions:
contents: write # to check out repo (actions/checkout) and to create releases (changesets/action/publish)
steps:
- name: Check out repo
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Build packages
run: npm build
- name: Publish packages
uses: changesets/action/publish@v2Version Only
If you do not plan to publish your packages, or only using Changesets to manage changelogs, you can configure a workflow that only versions your packages.
name: Version
on:
push:
branches:
- main
# recommended: reset permissions and explicitly specify for each job
permissions: {}
# recommended: avoid concurrent runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # to check out repo (actions/checkout) and commit version changes (changesets/action/version)
pull-requests: write # to create pull request (changesets/action)
steps:
- name: Check out repo
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
package-manager-cache: false # recommended: avoid cache poisoning
- name: Install dependencies
run: npm install
- name: Build packages
run: npm build
- name: Version packages
uses: changesets/action@v2
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # pass the token hereAdditional Notes
Understanding npm Authentication
When using actions/setup-node with the registry-url option, internally it will set up a .npmrc file that looks like this:
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}This syntax allows to authenticate with the npm registry only when the NODE_AUTH_TOKEN environment variable is set, which is the safer approach than storing the token directly in the .npmrc file.
For advanced use cases, you can also set up the ~/.npmrc file manually. Make sure to remove the registry-url option from actions/setup-node to prevent conflicts with your custom .npmrc file.
For example, if you need to publish different scopes to different registries, you can set up the ~/.npmrc file like below:
- run: |
cat << 'EOF' > ~/.npmrc
# For unscoped packages, publish to the default npm registry
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
# For @foo/* packages, publish to a custom registry
@foo:registry=https://my-registry.com/
//my-registry.com/:_authToken=${NODE_FOO_AUTH_TOKEN}
# For @bar/* packages, publish to the GitHub Package Registry
@bar:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
EOFRun GitHub Actions for Version PRs
By default, PRs created by GitHub Actions do not trigger workflows for the PR. However, you can still manually trigger the workflow from the UI.
To always automatically run workflows for version PRs, you need to use either a personal GitHub token or a GitHub App token, and pass it to the Changesets GitHub Action. To set up with a GitHub App token:
jobs:
# ...
version:
# ...
runs-on: ubuntu-latest
permissions:
contents: read # to check out repo (actions/checkout)
steps:
# ...
- name: Create GitHub App Token
uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write # to commit version changes (changesets/action/version)
permission-pull-requests: write # to create pull request (changesets/action/version)
- name: Version packages
uses: changesets/action/version@v2
with:
github-token: ${{ steps.app-token.outputs.token }}Check the actions/create-github-app-token documentation for details on setting up vars.APP_CLIENT_ID and secrets.APP_PRIVATE_KEY.
Also, make sure version commits and merge commits do not contain [skip ci] (or any variants) that would skip the workflow from running. This may happen when configuring the commit-message for changesets/action/version or the Changesets commit config's skipCI option, which could cause tests or publish steps to not execute.
pnpm
An .npmrc file in a project directory with pnpm does not support environment variables due to security reasons. As such, it's recommended to set up in the home directory instead. This is also the general recommendation for other package managers to not mix potential existing config setups in projects.
yarn
Yarn does not support the .npmrc file. To set up authentication for yarn, use a ~/.yarnrc.yml file instead:
- run: |
cat << 'EOF' > ~/.yarnrc.yml
npmAuthToken: "${NODE_AUTH_TOKEN}"
EOFFor advanced use cases, such as if you need to publish different scopes to different registries, you can set up the ~/.yarnrc.yml file like below:
- run: |
cat << 'EOF' > ~/.yarnrc.yml
npmAuthToken: "${NODE_AUTH_TOKEN}"
npmScopes:
foo:
npmRegistryServer: "https://my-registry.com/"
npmAuthToken: "${NODE_FOO_AUTH_TOKEN}"
bar:
npmRegistryServer: "https://npm.pkg.github.com/"
npmAuthToken: "${GITHUB_TOKEN}"
EOF