Skip to main content

Publishing from a Monorepo

Publishing multiple packages from a monorepo adds complexity: coordinating versions, managing interdependencies, and automating releases across packages.

The Challengeโ€‹

In a monorepo with packages A, B, and C where B depends on A:

  • When A changes, B's dependency on A must be updated
  • Both A and B need new versions published
  • Consumers need consistent, compatible versions
  • CI must publish packages in the correct order

Workspace Protocolsโ€‹

pnpm and Yarn (Berry) use the workspace: protocol to link local packages during development:

{
"dependencies": {
"@my-org/package-a": "workspace:^"
}
}

At publish time, the workspace protocol is replaced with the actual version:

ProtocolResolves to
workspace:*Exact version (e.g., 1.2.3)
workspace:^Caret range (e.g., ^1.2.3)
workspace:~Tilde range (e.g., ~1.2.3)
info

pnpm and Yarn (Berry) support the workspace: protocol; npm does not โ€” npm workspaces save plain semver ranges and publish them as-is, with no publish-time rewriting. pnpm has the most mature publish-time rewriting. See npm Workspaces.

Changesets is the most popular tool for monorepo versioning and publishing. It uses a file-based approach where developers declare the impact of their changes.

Setupโ€‹

npm install -D @changesets/cli
npx changeset init

This creates a .changeset/ directory in your repo root.

Workflowโ€‹

1. Add a changeset (per PR)โ€‹

npx changeset

This interactive prompt asks:

  • Which packages changed?
  • What type of bump? (major/minor/patch)
  • A summary of the change

It creates a markdown file in .changeset/:

---
"@my-org/package-a": minor
"@my-org/package-b": patch
---

Added streaming support to package-a. Updated package-b to use the new API.

2. Version packagesโ€‹

npx changeset version

This consumes all pending changesets and:

  • Bumps package versions
  • Updates interdependency versions
  • Generates/updates CHANGELOG.md files

3. Publishโ€‹

npx changeset publish

Publishes all packages with new versions to the registry.

Changesets with GitHub Actionsโ€‹

name: Release

on:
push:
branches: [main]

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: npx changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

The changesets/action creates a "Version Packages" PR that accumulates changesets. Merging it triggers publishing.

Lernaโ€‹

Lerna was the original monorepo publishing tool. While less common for new projects, it's still maintained (now by Nx):

npx lerna publish

Lerna can use either fixed versioning (all packages share a version) or independent versioning.

Nx Releaseโ€‹

Nx includes a built-in release command:

npx nx release

Features:

  • Automatic version detection from conventional commits
  • Dependency graph-aware publishing (correct order)
  • Changelog generation
  • Dry-run mode
# Preview what would happen
npx nx release --dry-run

# Release only affected packages
npx nx release --projects=packages/*

CI/CD Pipeline Patternsโ€‹

Publish on merge to mainโ€‹

on:
push:
branches: [main]

jobs:
publish:
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx changeset publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Canary releases from PRsโ€‹

Publish preview versions from pull requests for testing:

npx changeset version --snapshot canary
npx changeset publish --tag canary

This produces versions like 1.2.4-canary-20260611.0 that won't affect the latest tag.

Pre-release channelsโ€‹

For alpha/beta releases alongside stable:

npx changeset pre enter beta
# ... continue adding changesets ...
npx changeset version # Produces 2.0.0-beta.0, etc.
npx changeset publish --tag beta
npx changeset pre exit # Return to stable releases

Tipsโ€‹

  • Always build before publishing โ€” Add prepublishOnly scripts to each package
  • Use --dry-run โ€” Test publishing without actually hitting the registry: npm publish --dry-run
  • Publish in topological order โ€” Tools like Changesets and Nx handle this automatically based on the dependency graph
  • Pin workspace dependencies for publishing โ€” With pnpm or Yarn Berry, use workspace:^ rather than workspace:* so consumers get proper semver ranges (npm has no workspace: protocol โ€” its ranges are published as-is)
  • Automate changelogs โ€” Changesets generates per-package CHANGELOGs automatically from changeset descriptions