Operations 19 min read

Mastering Multi‑Package Publishing in Monorepos with Rush and pnpm

This article explains how to efficiently manage multi‑package publishing in large monorepos using Rush and pnpm, covering workspace protocol, changefile generation, cascading version updates, workflow automation, and release acceleration techniques to reduce build and publish times.

ELab Team
ELab Team
ELab Team
Mastering Multi‑Package Publishing in Monorepos with Rush and pnpm

Preface

In May we shared an application‑level Monorepo optimization plan that described problems and solutions for a Yarn + Lerna monorepo, but it did not cover package publishing. At that time most work was on app development, and simple package publishing could be done with npm publish.

After migrating another repository into the monorepo, package development became a major part of the codebase (over a million lines and more than 100 projects). The multi‑package publishing experience was poor in three areas:

Rush commands differed significantly from Lerna and had sparse documentation, making them hard to adopt.

The publishing process was not standardized and relied on manual CLI commands.

There was no standard development workflow.

This talk presents the best practices we discovered for multi‑package publishing in a monorepo.

Workspace Protocol (workspace:)

Before discussing, understand the workspace protocol using pnpm. By default, if a package version in the workspace satisfies the declared range, pnpm links the local package. For example, if [email protected] exists in the monorepo and another project bar depends on "foo": "^1.0.0", bar will use the workspace copy. If bar requests "foo": "2.0.0", pnpm will download [email protected] from the registry, introducing uncertainty.

When using the workspace protocol, pnpm refuses to resolve any version that does not exist in the workspace. Therefore, setting "foo": "workspace:2.0.0" will cause the installation to fail because [email protected] is not present.

Multi‑Package Publishing

Basic Operations

Unlike traditional single‑repo single‑package publishing, a monorepo allows convenient multi‑package publishing.

rush change

In a Rush monorepo, rush change starts the publishing flow. It generates a <branchname>-<timestamp>.json file (referred to as changefile.json) that is later consumed by rush version and rush publish.

The changefile.json generation process:

Detect differences between the current branch and the target branch (usually master) using git diff and filter projects with changes.

Interactively ask for information such as version bump strategy and a brief description of the change for each affected project.

Generate a changefile.json under common/changes for each package based on the provided information.

Note: In the screenshot the type field is none , which means the change will be rolled into the next patch, minor, or major release. If a package only has type: none , it will not trigger a version bump.

The type: none feature lets us merge completed packages that do not need to be released in the next cycle, postponing their publication until a changefile with a non‑ none type appears.

rush version and rush publish

rush version

or rush publish --apply updates version numbers based on the generated changefile.json, following semver. Dependent packages may also have their versions bumped. rush publish --publish publishes the packages indicated by the changefiles.

The Rush flow is essentially the same as the popular Changesets tool, so the approach can be reused for pnpm‑based monorepos.

A way to manage versioning and changelogs with a focus on monorepos.

Changesets: Popular Monorepo Publishing Tool

Cascading Publishing

When updating a version, not only the target package but also its upstream packages may be updated, depending on how the upstream package.json references the current package.

Example: @modern-js/plugin-tailwindcss depends on @modern-js/utils via "workspace:^1.0.0".

{
  "name": "@modern-js/plugin-tailwindcss",
  "version": "1.0.0",
  "dependencies": {
    "@modern-js/utils": "workspace:^1.0.0"
  }
}
{
  "name": "@modern-js/utils",
  "version": "1.0.0"
}

If @modern-js/utils updates to 1.0.1, the upstream package does not need a version bump because ^1.0.0 satisfies 1.0.1.

If @modern-js/utils updates to 2.0.0, the upstream package must bump to 1.0.1 and change its dependency to "workspace:^2.0.0" to reference the new version.

{
  "name": "@modern-js/plugin-tailwindcss",
  "version": "1.0.1",
  "dependencies": {
    "@modern-js/utils": "workspace:^2.0.0"
  }
}

After updating versions, run rush publish --include-all to publish every package whose shouldPublish flag is true.

Unexpected Publishing

Using "workspace:*" for all internal references caused two problems:

When an app is released, it may unintentionally include packages still under development.

Any change in a lower‑level package forces an upstream package to be published, even if not needed.

Therefore, internal references should follow these guidelines:

Determine whether the latest workspace version is required.

If needed, use "workspace:^x.x.x" instead of "workspace:*" to avoid unnecessary releases, unless the packages are always released together.

For loosely coupled projects, it is better to depend on stable npm versions rather than workspace links.

Workflow

Key points during development:

When merging feature work into master, generate a type: none changefile to prevent premature publishing.

Generate type: major/minor/patch changefiles on a test branch for actual release testing.

Pipeline Details

Canary (Test) Release

Identify packages to publish from changefile.json.

Install only the required package dependencies: rush install -t package1 -t package2.

Build the targeted packages: rush build -t package1 -t package2.

Run rush publish --prerelease-name [canary.x] --apply to update versions.

Publish the changed packages with

rush publish --publish --tag canary --include-all --set-access-level public

.

Notify relevant teams via a bot.

Official (Release) Version

Identify packages to publish from changefile.json.

Install required dependencies.

Build the targeted packages.

Create a release branch to host the publishing commits.

Run

rush version --bump --target-branch [source-branch] --ignore-git-hooks

to apply changefiles and generate CHANGELOG.md.

Run rush update on the release branch to sync the lockfile.

Publish to npm with

rush publish --apply --publish --include-all --target-branch [source-branch] --add-commit-details --set-access-level public

.

Create a merge request to merge the release branch back into master.

Notify teams via a bot.

Publish Acceleration

The first three steps of any publish flow are identical: identify packages, install dependencies, and build them. Initially we performed a full monorepo install and build, which became a bottleneck as the repository grew.

Monorepos must solve scalability: larger projects lead to slower dependency installation, builds, and tests. “On‑demand” installation is the key. pnpm already supports on‑demand installs, but large monorepos still need additional tooling, which is why we introduced Rush.

Before optimization, a single package publish took about 12 minutes, even for a trivial change like console.log("hello world"). As the project grew, this time only increased.

Rush changes version numbers early, allowing us to pre‑compute which packages need to be installed and built, dramatically reducing overall publish time.

Auxiliary Commands

rush change‑extra

This command addresses lockfile‑related issues by generating changefile.json for packages that have no code changes but still need to be published.

Normally rush change compares the current branch with master and interactively creates changefiles for changed projects. However, when using "workspace:^x.x.x", only major updates trigger upstream version bumps. Packages that remain unchanged may be locked by downstream lockfiles, preventing their publication. rush change‑extra solves this by forcing a changefile for such packages.

Conclusion

Starting from basic Rush publishing commands, we identified common pain points and presented a complete solution that leverages “on‑demand” principles to accelerate online releases in large monorepos.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MonorepopnpmVersioningrushpackage publishingworkspace protocol
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.