Skip to content

Releasing & Publishing ​

The full release cycle: bump a version through a PR/MR, tag it, create a release, and publish to npm — as four separate, composable steps, plus sync-deps to fix up any other local package left pointing at a now-stale version afterward. Bumping a batch of packages and then publishing or releasing only some of them are different decisions that don't always happen at the same time, so they're never forced together. Works against GitHub or GitLab, autodetected per repo — see Config & Discovery.

bump ​

Pick packages, bump their version (patch by default), open a PR/MR, merge it into the default branch, and tag the release.

bash
polyrepo bump [options]
  1. Shows a checkbox of packages with their current version and the version they'd bump to (1.2.9 → 1.2.10 for a patch bump — --minor/--major bump that part instead, resetting the parts below it to 0, same as any semver tool). Packages with a dirty working tree are marked — they'll be skipped. The highlighted package's description shows what's actually changed since the last git tag — computed for every package in parallel.
  2. After confirming, for each selected package, one at a time:
    1. git fetch origin → git checkout <default branch> → git merge --ff-only origin/<default branch> (the bump branch is always created from an up-to-date default branch, not whatever branch the repo happened to be on);
    2. checks the state of a previous attempt — is there already a merged PR/MR, an open one, or just a pushed branch named <new-version>-version-bump (or <sanitized-name>-<new-version>-version-bump for a pnpm workspace member, so two packages bumped to the same version in the same repo never fight over one branch name). Depending on what's found, it resumes from the right place instead of failing on "branch already exists" or opening a duplicate one;
    3. if package.json isn't bumped yet on the branch, its "version" field is updated (a text-level replace — formatting and field order are left alone). If the package already has a CHANGELOG.md, a draft ## [x.y.z] - YYYY-MM-DD entry (Keep a Changelog style) is added too, listing commits since the last tag — a starting draft to review, not a finished changelog;
    4. gh pr create (GitHub) or glab mr create (GitLab) against the default branch (if there isn't one already);
    5. with --wait-checks: waits for CI before merging — see the option below, the exact mechanism differs by host; no checks configured isn't an error;
    6. gh pr merge / glab mr merge — through a PR/MR, not a direct push;
    7. local default branch is synced to the just-merged PR/MR;
    8. git tag v<new-version> is created and pushed if it doesn't already exist — <name>@<new-version> for a pnpm workspace member, for the same collision-avoidance reason as the branch name above.
  3. Any failure along the way marks that package ✗ with a clear message and moves on to the next one, without aborting the whole run.
  4. Once every selected package is processed, a separate check across all packages (not just the ones just bumped) reports any local package whose dependency range no longer matches a bumped package's new version — nothing is changed automatically here (see sync-deps for the fix).
  5. If at least one package was actually tagged (and it wasn't a --dry-run), whether to publish those packages to npm right now is asked once for the whole batch — unless --publish was given, which skips the question and does it anyway (skipped entirely under --yes without --publish, same as tag's release prompt). Either way this is exactly publish run against exactly the packages that were just bumped — see "Publishing" below.

Publishing ​

Without --publish, the question above is the only place bump touches npm. With it (or answering yes), it's exactly polyrepo publish for exactly the packages that were just bumped: npm login is checked once up front, a pnpm workspace member's dependencies are (re)linked first if needed, and a prerelease version (e.g. 2.0.0-beta.0) publishes under the next dist-tag instead of latest automatically — same rules as publish itself. Only packages that actually got tagged this run are ever offered — one skipped for being dirty, or that failed partway through, is never published.

Example output — a --dry-run, nothing exists yet:

[1/1] os-detect  2.1.5 → 2.1.6
  $ git fetch origin
  $ git checkout master
    Your branch is up to date with 'origin/master'.
    Already on 'master'
  $ git merge --ff-only origin/master
    Already up to date.
  ✓ master is up to date.
  [dry-run] would create branch 2.1.6-version-bump, ensure version 2.1.6 (+ a CHANGELOG.md entry if one exists), commit/push if needed, then open + merge a PR, then tag v2.1.6.

Re-running bump on a package where a previous attempt already went all the way through is much shorter — everything already done is just confirmed, not redone:

[1/1] vue-toast-kit  1.0.7 → 1.0.8
  ✓ master is up to date.
  ✓ Already merged as PR #12 — master already has it.
  ✓ Tag v1.0.8 already exists on origin.

(On a GitLab repo, this reads "Already merged as PR #12" the same way — the provider layer always calls it "PR" in these messages regardless of host, the underlying object is a merge request.)

--prerelease, or --preid paired with --major/--minor, compute the target version differently but go through the exact same branch → PR → merge → tag steps:

[1/1] os-detect  2.1.5 → 2.1.6-beta.0
  $ git fetch origin
  $ git checkout master
    Your branch is up to date with 'origin/master'.
    Already on 'master'
  $ git merge --ff-only origin/master
    Already up to date.
  ✓ master is up to date.
  [dry-run] would create branch 2.1.6-beta.0-version-bump, ensure version 2.1.6-beta.0 (+ a CHANGELOG.md entry if one exists), commit/push if needed, then open + merge a PR, then tag v2.1.6-beta.0.

Options ​

--dry-run ​

Prints the plan for each package, changes and pushes nothing — including the CHANGELOG.md entry, CI wait, and git tag.

--minor ​

Bump the minor version instead of patch (e.g. 1.2.9 → 1.3.0).

--major ​

Bump the major version instead of patch (e.g. 1.2.9 → 2.0.0).

--prerelease ​

Bump (or start) a prerelease instead of patch/minor/major (e.g. 1.2.9 → 1.2.10-alpha.0, or 1.2.10-alpha.0 → 1.2.10-alpha.1). Combine with --preid to name it — node-semver decides whether that means adding e.g. -alpha.0 to the current version or advancing an existing prerelease's number.

--preid <name> ​

Prerelease identifier for --prerelease, or paired with --minor/--major for a preminor/premajor prerelease instead (e.g. --major --preid beta on 1.2.9 → 2.0.0-beta.0). Defaults to alpha.

--custom-version <version> ​

Set an exact version instead of computing one. Only valid together with --packages naming a single package — applying one literal version to several packages at once is never actually what you want.

--packages <a,b,c> ​

Comma-separated package dir names instead of the interactive checkbox — for scripts. Unknown names are printed as a warning and skipped.

--yes ​

Skip the "proceed?" confirmation.

--wait-checks ​

Wait for CI before merging; don't merge if it fails. On GitHub this is gh pr checks --watch, scoped to the PR. GitLab has no equivalent scoped to an MR — this uses glab ci status --branch --wait on the bump branch's pipeline instead, same idea. No checks configured isn't an error either way — it just doesn't wait.

--publish ​

After tagging, also publish to npm — without asking (see "Publishing" above).

Example:

bash
# dry run first — nothing is pushed, committed, merged, or tagged
polyrepo bump --dry-run

# bump or advance a beta prerelease
polyrepo bump --prerelease --preid beta

# start a premajor release candidate (e.g. 2.0.0-rc.0)
polyrepo bump --major --preid rc

# set an exact version for one package, e.g. for a hotfix
polyrepo bump --packages os-detect --custom-version 2.1.6-hotfix.1

# bump, tag, and publish, non-interactively
polyrepo bump --publish --yes

# fully non-interactive, for a script/CI
polyrepo bump --packages vue-toast-kit,os-detect --yes

tag ​

For a package whose version was already bumped some other way (not through bump, or before it started tagging), release has nothing to work with — there's no tag for the current version yet. tag puts the missing tag on the current version without bumping it again or opening a PR.

bash
polyrepo tag [options]
  1. Fetches each repo's origin default branch (once per physical repo), then checks each package (in parallel) for whether a tag v<local version> (<name>@<local version> for a pnpm workspace member) already exists. A package whose local version differs from what's actually on origin is flagged right here — worth knowing, since the next step fast-forwards local to origin's default branch before tagging, so the tag about to be created may not end up naming the version that's actually tagged.
  2. Shows a checkbox: package, version, tag — with a ⚠ origin has <version> note on anything that differs from origin. Untagged packages are pre-selected; already-tagged ones can still be picked manually (harmless — it just confirms the tag is there).
  3. After confirming, for each selected package: syncs the default branch (same as bump), then re-reads the version from package.json now that local matches origin exactly — the version on disk can have changed if local wasn't already on the default branch, or was behind. Tagging always names the version that's actually here now, not whatever was there before the sync; a warning is printed if that changed which tag gets created. Creates and pushes the (possibly renamed) tag if it's missing.
  4. If at least one package was actually tagged (and it wasn't a --dry-run), it asks: "Create a release for the N package(s) just tagged?" — answering yes runs the same process as release for exactly those packages.

Example output — the current version already has a tag, so there's nothing to do but confirm it:

[1/1] os-detect  v2.1.5
  $ git fetch origin
  $ git checkout master
    Your branch is up to date with 'origin/master'.
    Already on 'master'
  $ git merge --ff-only origin/master
    Already up to date.
  ✓ master is up to date.
  ✓ Tag v2.1.5 already exists on origin.

Example output — local was behind origin, so syncing the default branch changed what's actually being tagged:

[1/1] os-detect  v2.1.5  (origin has 2.1.6)
  $ git fetch origin
  $ git checkout master
  $ git merge --ff-only origin/master
    Fast-forwarded to origin/master.
  ✓ master is up to date.
  ! Version changed from 2.1.5 to 2.1.6 after syncing with origin — tagging v2.1.6 instead of v2.1.5.
  ✓ Tagged and pushed v2.1.6.

Options ​

--dry-run ​

Prints the plan; tags, pushes, and releases nothing.

--packages <a,b,c> ​

Package list instead of the interactive checkbox.

--yes ​

Skip the "proceed?" confirmation (the release question is skipped too — no release is created unless --release is also given).

--release ​

Create a release right after tagging, without asking — for scripts.

Example:

bash
polyrepo tag --packages vue-toast-kit,os-detect --yes --release

release ​

Pick packages and create a release for their current version's tag.

bash
polyrepo release [options]
  1. Checks each package (in parallel) for a v<local version> tag (<name>@<local version> for a pnpm workspace member) on origin (the one bump or tag creates) and whether that tag already has a release.
  2. Shows a checkbox: package and its tag. Packages with no tag for their current version show up disabled ("run bump first"); already-released ones are selectable but unchecked, in case you want to re-run it.
  3. After confirming, for each selected package: gh release create <tag> --verify-tag (GitHub) or glab release create <tag> (GitLab) — the checkbox already only offers packages with an existing tag, so this never invents one; --verify-tag is GitHub's own second guarantee of the same thing, GitLab has no equivalent flag. Release notes come from the matching CHANGELOG.md section when there is one. Otherwise: GitHub's own --generate-notes (a summary of merged PRs/commits); GitLab has no equivalent, so this falls back to a plain commit-log listing since the last tag instead.

Example output — on a GitHub repo:

[1/1] os-detect  v2.1.5
  ✓ No changelog entry — using --generate-notes.
  $ gh release create v2.1.5 --verify-tag --title os-detect@2.1.5 --generate-notes
  ✓ Created release os-detect@2.1.5.

Options ​

--dry-run ​

Prints what would be created, without actually creating any release.

--packages <a,b,c> ​

Package list instead of the interactive checkbox.

--yes ​

Skip the "proceed?" confirmation.

Example:

bash
polyrepo release --packages vue-toast-kit,os-detect --yes

publish ​

Pick packages and run npm publish — pre-selects ones ahead of the registry.

bash
polyrepo publish [options]
  1. Fetches each repo's origin default branch (once per physical repo), then checks each package's registry version (npm view <pkg> version) against its local version — in parallel. A package marked "private": true (including a pnpm workspace's own root) is excluded entirely, before the checkbox is even built — it was never meant to reach the registry.
  2. Shows a checkbox: registry version → local version, plus which dist-tag it would publish under (see --dist-tag below) and a ⚠ origin has <version> note when origin's default branch has a different version than local. Packages where the registry differs from local are pre-selected; already-published ones stay selectable (e.g. to republish after an unpublish).
  3. After confirming, checks npm whoami once for the whole batch (skipped under --dry-run) — not logged in runs npm login (a real terminal, so its browser-based OTP flow or credential prompt works normally) before publishing anything, instead of only finding out partway through the first package's npm publish.
  4. For each selected package, one at a time: npm publish (or npm publish --dry-run with the --dry-run flag) — or, for a pnpm workspace member, pnpm publish --no-git-checks instead, so a workspace:* dependency on a sibling package resolves to a real version number rather than failing the publish. Runs with a real terminal, not captured — an npm 2FA/OTP prompt works normally.

Example output — the middle section is npm's own build/test/pack output for the package, shown live in the real terminal, not something polyrepo captures or reformats:

[1/1] os-detect@2.1.5
  $ npm publish --dry-run
  … (the package's own build, tests, and npm's tarball contents listing)
  ✓ Published os-detect@2.1.5 (dry run).

publish never checks git state — same as a plain npm publish never has — it always publishes exactly what's on disk. It does warn (without blocking) right before the confirm prompt when a selected package isn't on its default branch, has uncommitted changes, or its local version differs from origin's:

! os-detect is on branch fix-typo, not master — publishing what's on disk anyway.
! vue-toast-kit has uncommitted changes — publishing what's on disk anyway.

Dist-tags ​

A prerelease version (anything with a -, e.g. 2.0.0-beta.0) publishes under the next dist-tag by default instead of npm's own default latest — otherwise it would become the version anyone installing without pinning a version gets, which is essentially never the intent of a prerelease. --dist-tag overrides this for every selected package (auto-detection included) — pass it explicitly to publish a stable version under something other than latest too (e.g. an LTS channel).

Options ​

--dry-run ​

npm publish --dry-run instead of a real publish.

--dist-tag <tag> ​

npm dist-tag to publish every selected package under (default: next for a prerelease version, otherwise npm's own default latest). See "Dist-tags" above.

--packages <a,b,c> ​

Package list instead of the interactive checkbox.

--yes ​

Skip the "proceed?" confirmation.

Example:

bash
polyrepo publish --packages vue-toast-kit,os-detect --yes
polyrepo publish --packages os-detect --dist-tag next   # force a specific dist-tag

sync-deps ​

The write side of the "Deps" column in list and the "Cross-package dependencies" section in doctor: a local package whose declared range on another local package no longer matches that package's current version (e.g. it still says ^1.1.0 after the dependency moved to 1.2.0). Every place that reports this stops at reporting — this is the one that fixes it.

bash
polyrepo sync-deps [options]
  1. Reads every discovered package (not just the ones named by --packages) — finding a stale range requires knowing every local package's current version, not just the ones being fixed.
  2. Shows a before/after checkbox for each stale range found: "depName" "oldRange" → "newRange", keeping each range's own style (^ stays ^, ~ stays ~, an exact pin stays exact — anything unusual falls back to a ^ range, visible in this same preview before anything is written). --packages narrows which dependent packages' issues are offered, same meaning as everywhere else in this CLI.
  3. After confirming, updates package.json in place for each selected issue — a text-level replace of the exact "depName": "oldRange" pair, so formatting, key order, and quote style elsewhere in the file are left untouched.

Only ever edits package.json on disk — no commit, no push, no branch/PR. Review the change afterward (git diff), then commit it the way each repo's rules allow with polyrepo commit.

Example output:

Sync local dependency ranges

  app-shell  @acme/ui-kit  "^1.1.0" → "^1.2.0"
  app-shell  @acme/api-client  "~2.0.0" → "~2.1.0"

✓ app-shell: "@acme/ui-kit" "^1.1.0" → "^1.2.0"
✓ app-shell: "@acme/api-client" "~2.0.0" → "~2.1.0"

Options ​

--dry-run ​

Print what would be updated, without actually writing any package.json.

--packages <a,b,c> ​

Only offer issues for these dependent packages — same meaning as everywhere else.

--yes ​

Skip the "proceed?" confirmation.

Example:

bash
polyrepo sync-deps                          # see what's stale, then update it
polyrepo sync-deps --dry-run                # print the plan, change nothing
polyrepo sync-deps --packages a,b --yes     # non-interactive, for scripts/CI