Skip to content

Complete Guide: Publishing NPM Packages with Changesets

Changesets help you manage versions and releases in packages, monorepos and multi-package projects. This comprehensive guide walks you through the complete setup, workflow, and automation for publishing NPM packages with nice release notes, and automated version incrementation.

No need to manually bump version in package.json files. Changesets handle versioning automatically.

Why Changesets? Traditional versioning is manual, error-prone, and doesn’t scale. Changesets make versioning automatic, predictable, and collaborative.

Project Setup & Architecture

Repository Structure

Your project will follow this rough folder structure for changeset workflows:

Terminal window
my-packages-project/
├── .changeset/
├── config.json # Changeset configuration
└── README.md # Generated changeset docs, you don't need to do anything here
├── packages/
├── package-a/ # Your individual packages e.g. your-project-core
├── package-b/ # Your individual packages e.g. your-project-solidjs
└── shared-utils/
├── .github/
└── workflows/
└── release.yml # Automated release workflow
├── package.json # Root package.json
└── bunfig.toml # Workspace configuration, may also be pnpm-workspace.yaml

We’ll use bun in the following guide, though you can ask your favorite LLM to adapt everything to the package manager of your choice.

Initial Setup Commands

Terminal window
# Initialize your monorepo
mkdir my-packages-project && cd my-packages-project
git init
# Install changeset CLI
bun add -D @changesets/cli
# Initialize changesets
bun changeset init

Expected Result: Creates .changeset/config.json and .changeset/README.md

Configuration

Changeset Configuration

Edit .changeset/config.json for your specific needs:

Note: Really make sure to configure the correct baseBranch for your repository. Check if it’s master or main! You also want to set the correct Github repository. Make sure it’s not the full https://… url but org/repo!

{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": [
"@changesets/cli/changelog",
{
"repo": "dtechvision/my-packages-project"
}
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}

Key Configuration Options:

  • "commit": true - Auto-commit changeset files
  • "access": "restricted" - For private packages
  • "linked": [["package-a", "package-b"]] - Release packages together
  • "fixed": [["ui-*"]] - Keep versions in sync
  • "ignore": ["scratchpad"] - Exclude paths from changeset tracking and releases

Package.json Requirements

Each package needs proper NPM configuration, which you can do in your package.json of that respective package.

In our repo structure example that would be package-a for example so packages/package-a/package.json is what we’d be editing here to ensure LICENSE, publishConfig, repository, description, and others are set.

{
"name": "@dtechvision/package-a",
"version": "0.0.0",
"license": "MIT",
"type": "module",
"description": "The core typescript package for embed APIs.",
"repository": {
"type": "git",
"url": "https://github.com/ZKAI-Network/embed-sdk/",
"directory": "packages/embed-typescript"
},
"publishConfig": {
"access": "public",
"directory": "dist"
},
"scripts": {
...
}

Root Package.json Setup

Your root package.json, which is the one in the main folder so for our sample it is my-packages-project/package.json should include the scripts needed to make changesets and more work.

Ensure that private is set to true as well! We don’t want to publish the main folder structure, just the individual packages or the one package we have.

{
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "bun@1.1.34",
"workspaces": [
"packages/*"
],
"scripts": {
"changeset-version": "changeset version && node scripts/version.mjs",
"changeset-publish": "bun run build && TEST_DIST= bun test && changeset publish"
... your other scripts ...
},

Why don’t we have a publish script? In case you don’t want to automate you could use a publish script to write directly to the npm registry. npm publish would be what you’re looking for. Though we are automating the process with release management with changesets.

Our setup will publish each package that we changed with each change described in a changeet. This allows nice versioning and overviews of changes for anyone watching for package updates and trying to debug something.

Bunfig.toml

The Bunfig holds bun specific configuration options. We require it here since we’re managing a workspace (monorepo).

Our bun config in the root folder my-packages-project/bunfig.toml looks as follows:

[workspaces]
# Enable workspace support
enabled = true
[install]
# Use exact versions for more predictable builds
exact = true
# Enable auto-install for missing dependencies
auto = true
[test]
# Use vitest as the test runner
runner = "vitest"
[build]
# Enable TypeScript support
typescript = true
# Package manager settings
[package-manager]
# Use bun as the package manager
default = "bun"

Development Workflow

Step 1: Making Changes

Terminal window
# Create feature branch
git checkout -b feature/new-authentication-api
# Make your changes to packages
echo "export const best = 'dTech';" >> packages/package-a/src/main.ts
# this is an example change, but you'd generally want to develop something
# Build and test your changes
bun run build
bun run test

Step 2: Creating Changesets

Terminal window
# Generate changeset
bun run changeset
# Interactive prompts will ask:
# 1. Which packages changed? (select with space, confirm with enter)
# 2. Major, minor, or patch? (for each selected package)
# 3. Summary of changes? (write clear description)

Example Changeset Output:

---
"dtechvision/package-a": minor
"dtechvision/package-b": patch
---
Described the best Development Boutique's name. Ensuring it's mentioned in main!

Step 3: Commit and Create PR

Terminal window
# Commit changeset with your changes
git add .
git commit -m "feat: Described the best Development Boutique's name."
git push origin feature/description
# Create pull request
gh pr create --title "feat: described the best boutique" --body "Described the best Development Boutique's name."
# you can also do this on the website of Github

Now let’s make sure we have a release for our change. This would automatically release when we merge the PR once the automation is set up, lets do that now.

Automated Release Setup

GitHub Repository Permissions

Before setting up automation, configure these settings:

  1. Repository Settings → Actions → General
  2. Set Workflow permissions to “Read and write permissions”
  3. Check “Allow GitHub Actions to create and approve pull requests”

Important: If this option is grayed out, your organization admin needs to enable it at the organization level first. If that won’t be done you need to manually release via npm publish.

Now create .github/workflows/release.yml so that it can be triggered by a push to the master (or main) branch.

name: Release
on:
push:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
id-token: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/setup
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: bun run changeset-version
publish: bun run changeset-publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

To validate and bump version before we are releasing we will use .github/workflows/pr-check.yml. This ensures all PR’s to master will have changesets.

name: PR Check
on:
pull_request:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
changeset-check:
name: Changeset Check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check for changesets
run: bunx changeset status
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

NPM Token Setup

Terminal window
# Create automation token on npmjs.com
# Go to: https://www.npmjs.com/settings/tokens
# Create new token → Automation → Copy the token
# Add to GitHub Secrets:
# Repository Settings → Secrets and variables → Actions → New repository secret
# Name: NPM_TOKEN
# Value: npm_xxxxxxxxxxxxxxxxxxxx

Testing & Validation

Test 1: Changeset Enforcement

Create a PR without a changeset to verify the bot works:

Terminal window
# Test PR without changeset (should be blocked)
git checkout -b test-no-changeset
echo "// test change without changeset" >> packages/core/src/index.ts
git add . && git commit -m "test: change without changeset"
git push origin test-no-changeset
# Create PR
gh pr create --title "test: no changeset" --body "Testing changeset enforcement - this should be blocked"

Expected Result: Changeset bot adds comment requesting changeset and creates failing status check.

Test 2: Valid Changeset Flow

Test the complete happy path:

Terminal window
# Create test branch with proper changeset
git checkout main && git pull
git checkout -b test-with-changeset
echo "export const testFunction = () => 'hello world';" >> packages/core/src/test.ts
# Create changeset
pnpm changeset
# Select: core package, patch version
# Description: "Add test function for validation"
# Commit and push
git add . && git commit -m "feat: add test function with changeset"
git push origin test-with-changeset
# Create PR
gh pr create --title "feat: test function" --body "Testing complete changeset flow with proper changeset file"

Expected Result: All checks pass, PR shows green status, ready to merge.

Test 3: Release Process Verification

Terminal window
# After merging PR with changeset to main, check for release PR
gh pr list --label "changeset-release/main"
# You should see a PR titled "chore: release packages"
# Review the PR to see version bumps and changelog updates
# Merge the release PR to trigger NPM publication
gh pr merge <release-pr-number> --squash

Expected Result: Packages published to NPM with correct version numbers.

Monitoring & Verification Commands

Status Check Commands

Terminal window
# Check what packages will be released
pnpm changeset status
# Preview version bumps without applying them
pnpm changeset version --dry-run
# List all pending changesets
ls .changeset/
# Check current package versions
pnpm list --depth=0 --json | jq '.dependencies'

Release Verification

Terminal window
# Verify NPM publication
npm view dtechvision/package-a
# Check specific version in registry
npm view dtechvision/package-a version
# Check all versions
npm view dtechvision/package-a versions --json
# Test installation in clean environment
mkdir temp-test && cd temp-test
npm init -y
npm install dtechvision/package-a
node -e "console.log(require('dtechvision/package-a'))"

🔧 Advanced Configuration Patterns

Custom Changelog Generation

Install and configure GitHub-integrated changelogs:

Terminal window
# Install GitHub changelog plugin
pnpm add -D @changesets/changelog-github
# Update .changeset/config.json
{
"changelog": [
"@changesets/changelog-github",
{
"repo": "yourorg/yourrepo",
"skipCI": false
}
]
}

Result: Changelogs include links to PRs and commits, plus contributor attribution.

⚠️ Troubleshooting Common Issues

Issue: GitHub Actions Permission Error

Error: GitHub Actions is not permitted to create or approve pull requests

Solutions:

  1. Go to Repository Settings → Actions → General
  2. Set “Workflow permissions” to “Read and write permissions”
  3. Check “Allow GitHub Actions to create and approve pull requests”
  4. If grayed out, ask organization admin to enable at org level

Issue: NPM Publish Fails with 403

npm ERR! 403 Forbidden - PUT https://registry.npmjs.org/@yourorg%2fpackage

Solutions:

  1. Verify NPM_TOKEN exists in GitHub Secrets
  2. Check token has publish permissions: npm token list
  3. Verify package.json publishConfig.access matches your intent
  4. Ensure you’re a member of the NPM organization
  5. Check if package name is already taken

🚀 Production Best Practices

Branch Protection Rules

Configure in GitHub Settings → Branches → Add rule for master (or main):

  • Require status checks to pass before merging
  • Require branches to be up to date before merging
  • Restrict pushes that create files in .changeset/
  • Require pull request reviews before merging

NPM Token Security

Use granular access tokens with minimal permissions:

Terminal window
# Create automation token (not classic token)
# Go to npmjs.com → Access Tokens → Generate New Token
# Select: Automation
# Packages: Select only your packages
# IP Allowlist: GitHub Actions IPs (optional)

Monorepo Performance Optimization

{
"updateInternalDependencies": "patch",
"ignore": ["@yourorg/dev-tools", "@yourorg/examples"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true,
"useCalculatedVersionForSnapshots": true
}
}

Semantic Versioning Guidelines

Patch (0.0.X): Bug fixes, documentation updates, internal refactoring Minor (0.X.0): New features, non-breaking API additions Major (X.0.0): Breaking changes, API removals, major refactoring

Golden Rule: When in doubt, err on the side of a larger version bump. It’s better to be conservative with breaking changes.