Monorepo Release Management: Changesets, Semantic Versioning, and Publish Pipelines
Manage releases in a monorepo: implement Changesets for versioning and changelogs, configure automated publish pipelines with GitHub Actions, handle peer dependency bumps, publish to npm with provenance, and coordinate breaking changes across packages.
Monorepo release management has two hard problems: coordinating version bumps across packages that depend on each other, and generating accurate changelogs from hundreds of commits. Changesets solves both by asking developers to declare the intent of their changes in small markdown files, then automating the version calculation and changelog generation from those declarations.
Changesets Workflow
Developer makes a change:
1. git add, git commit (code change)
2. pnpm changeset (declares what changed and semver impact)
โ creates .changeset/purple-lions-cry.md
When ready to release:
3. pnpm changeset version (reads all .changeset files, bumps versions, generates CHANGELOG.md)
4. pnpm changeset publish (builds packages, publishes to npm)
Setup
# Install Changesets
pnpm add -D @changesets/cli @changesets/changelog-github
# Initialize (creates .changeset/config.json)
pnpm changeset init
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "viprasol/design-system" }
],
"commit": false,
"fixed": [],
// Linked: packages in the same group always share the same version
"linked": [
["@viprasol/tokens", "@viprasol/components", "@viprasol/icons"]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@viprasol/docs"] // Never publish the docs app
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Writing Changesets
# Developer runs this after making changes
pnpm changeset
# Interactive prompt:
# ? Which packages would you like to include?
# โ @viprasol/components
# โฏ @viprasol/tokens
# ? Which type of change is this for @viprasol/components?
# โ patch โ Bug fixes (0.0.X)
# โ minor โ New features, backwards-compatible (0.X.0)
# โ major โ Breaking changes (X.0.0)
# ? Please enter a summary for this change:
# > Add `size` prop to Button component with sm/md/lg/xl variants
<!-- .changeset/graceful-lions-run.md (auto-named) -->
---
"@viprasol/components": minor
---
Add `size` prop to `Button` component with `sm`, `md`, `lg`, and `xl` variants.
Previously all buttons used a single fixed size. The default is `md`, maintaining
backwards compatibility.
Automated Release Pipeline
# .github/workflows/release.yml
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Push version commits and tags
pull-requests: write # Create the "Version Packages" PR
id-token: write # npm provenance (signed publish)
steps:
- uses: actions/checkout@v4
with:
# Full history needed for changelog generation
fetch-depth: 0
# Use a PAT so pushed commits trigger other workflows
token: ${{ secrets.RELEASE_BOT_PAT }}
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all packages
run: pnpm -r build
- name: Run tests
run: pnpm -r test
# The Changesets action does two things:
# 1. If changesets exist: creates/updates a "Version Packages" PR
# 2. If "Version Packages" PR is merged: publishes to npm
- name: Create Release PR or Publish
id: changesets
uses: changesets/action@v1
with:
version: pnpm changeset version
publish: pnpm changeset publish
commit: "chore: release packages"
title: "chore: release packages"
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_PAT }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Provenance: signed attestation linking npm publish to GitHub Actions run
NPM_CONFIG_PROVENANCE: "true"
# Post release: create GitHub Releases for each published package
- name: Create GitHub Releases
if: steps.changesets.outputs.published == 'true'
uses: actions/github-script@v7
with:
script: |
const publishedPackages = JSON.parse('${{ steps.changesets.outputs.publishedPackages }}');
for (const pkg of publishedPackages) {
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `${pkg.name}@${pkg.version}`,
name: `${pkg.name} v${pkg.version}`,
generate_release_notes: false,
});
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Pre-Release Versions
# Enter pre-release mode for a major version
pnpm changeset pre enter alpha
# Any changesets added while in pre mode become pre-releases
# e.g., @viprasol/components 2.0.0-alpha.0, 2.0.0-alpha.1, etc.
pnpm changeset version # Bumps to 2.0.0-alpha.1
pnpm changeset publish # Publishes with --tag alpha
# Exit pre-release mode for final release
pnpm changeset pre exit
pnpm changeset version # Finalizes to 2.0.0
pnpm changeset publish # Publishes to latest tag
Handling Breaking Changes
# Breaking change workflow:
# 1. Add major changeset
pnpm changeset
# Select: major
# Message: "BREAKING: Remove deprecated `color` prop from Button. Use `variant` instead."
# 2. Optionally add a migration guide file
cat > .changeset/migration-v2.md << 'EOF'
# Migration Guide: @viprasol/components v2.0
Breaking Changes
Button: color prop removed
Before:
<Button color="blue">Click me</Button>
After:
<Button variant="primary">Click me</Button>
The variant prop supports: primary, secondary, outline, ghost, destructive.
EOF
---
## Package.json for Published Packages
```json
// packages/components/package.json
{
"name": "@viprasol/components",
"version": "1.4.2",
"description": "Viprasol design system components",
"homepage": "https://design.viprasol.com",
"repository": {
"type": "git",
"url": "https://github.com/viprasol/design-system",
"directory": "packages/components"
},
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"!dist/**/*.test.*",
"CHANGELOG.md"
],
"sideEffects": false,
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Verifying Published Packages
# Check npm provenance (signed attestation)
npm audit signatures @viprasol/components
# Verify package contents before release
pnpm pack --dry-run
# Lists all files that would be published โ check for accidental inclusions
# View the generated CHANGELOG before releasing
cat packages/components/CHANGELOG.md | head -50
# Check published versions
npm view @viprasol/components versions --json
# Deprecate a version if needed
npm deprecate @viprasol/components@1.3.0 "Security fix in 1.3.1, please upgrade"
Canary Releases from Feature Branches
# .github/workflows/canary.yml
name: Canary Release
on:
push:
branches:
- "feat/**"
- "fix/**"
jobs:
canary:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- run: pnpm install --frozen-lockfile
- run: pnpm -r build
- name: Publish canary
run: |
# Set version to canary with git SHA suffix
SHA=$(git rev-parse --short HEAD)
pnpm -r exec -- node -e "
const pkg = require('./package.json');
if (pkg.private) process.exit(0);
pkg.version = pkg.version + '-canary.' + process.env.SHA;
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
pnpm changeset publish --no-git-tag --tag canary
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
SHA: ${{ github.sha }}
See Also
- TypeScript Monorepo with Nx โ monorepo tooling
- API Documentation with OpenAPI โ versioning docs
- API Versioning Strategies โ semver for APIs
- CI/CD Pipeline Setup โ pipeline patterns
Working With Viprasol
Release management across a monorepo becomes a real engineering problem once you have multiple teams and packages with interdependencies. We set up Changesets pipelines, configure automated publish workflows with provenance signing, and establish the release conventions that keep your component library and shared packages easy to maintain.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.