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:
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
# Initialize your monorepomkdir my-packages-project && cd my-packages-projectgit init
# Install changeset CLIbun add -D @changesets/cli
# Initialize changesetsbun 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 supportenabled = true
[install]# Use exact versions for more predictable buildsexact = true# Enable auto-install for missing dependenciesauto = true
[test]# Use vitest as the test runnerrunner = "vitest"
[build]# Enable TypeScript supporttypescript = true
# Package manager settings[package-manager]# Use bun as the package managerdefault = "bun"
Development Workflow
Step 1: Making Changes
# Create feature branchgit checkout -b feature/new-authentication-api
# Make your changes to packagesecho "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 changesbun run buildbun run test
Step 2: Creating Changesets
# Generate changesetbun 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
# Commit changeset with your changesgit add .git commit -m "feat: Described the best Development Boutique's name."git push origin feature/description
# Create pull requestgh 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:
- Repository Settings → Actions → General
- Set Workflow permissions to “Read and write permissions”
- 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: Releaseon: 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
# 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:
# Test PR without changeset (should be blocked)git checkout -b test-no-changesetecho "// test change without changeset" >> packages/core/src/index.tsgit add . && git commit -m "test: change without changeset"git push origin test-no-changeset
# Create PRgh 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:
# Create test branch with proper changesetgit checkout main && git pullgit checkout -b test-with-changesetecho "export const testFunction = () => 'hello world';" >> packages/core/src/test.ts
# Create changesetpnpm changeset# Select: core package, patch version# Description: "Add test function for validation"
# Commit and pushgit add . && git commit -m "feat: add test function with changeset"git push origin test-with-changeset
# Create PRgh 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
# After merging PR with changeset to main, check for release PRgh 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 publicationgh pr merge <release-pr-number> --squash
Expected Result: Packages published to NPM with correct version numbers.
Monitoring & Verification Commands
Status Check Commands
# Check what packages will be releasedpnpm changeset status
# Preview version bumps without applying thempnpm changeset version --dry-run
# List all pending changesetsls .changeset/
# Check current package versionspnpm list --depth=0 --json | jq '.dependencies'
Release Verification
# Verify NPM publicationnpm view dtechvision/package-a
# Check specific version in registrynpm view dtechvision/package-a version
# Check all versionsnpm view dtechvision/package-a versions --json
# Test installation in clean environmentmkdir temp-test && cd temp-testnpm init -ynpm install dtechvision/package-anode -e "console.log(require('dtechvision/package-a'))"
🔧 Advanced Configuration Patterns
Custom Changelog Generation
Install and configure GitHub-integrated changelogs:
# Install GitHub changelog pluginpnpm 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:
- Go to Repository Settings → Actions → General
- Set “Workflow permissions” to “Read and write permissions”
- Check “Allow GitHub Actions to create and approve pull requests”
- 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:
- Verify
NPM_TOKEN
exists in GitHub Secrets - Check token has publish permissions:
npm token list
- Verify package.json
publishConfig.access
matches your intent - Ensure you’re a member of the NPM organization
- 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:
# 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.