Release branches in a monorepo Jump to heading

In a single-product repository, a release branch means “this is version 2.4”. In a monorepo, it is less clear. Is release/2.4 a release of the web app, the mobile backend, the billing service, or all of them? If it is all of them, every team is tied to the same release cadence, and a fix one team needs forces a release branch for everyone. If each product cuts its own, release branches multiply and shared libraries end up in many versions at once. The workable middle ground is per-product release branches, scoped by name and by path: each product cuts its own, backports touch only that product’s paths and the shared libraries it uses, and rules prevent a product’s release branch from becoming a place where unrelated code changes. This page sets that up, within monorepo branch topology.

When to use this approach Jump to heading

  • Several independently released products live in one repository.
  • Some products need release branches — for stabilisation or long-term support — and others release continuously.
  • A shared release branch forces teams onto one cadence.
  • Packages are versioned independently, as in tagging packages independently in a monorepo.

Step 1 — Name release branches by product Jump to heading

Include the product in the branch name, so the branch states what it releases and rules can target it.

git switch -c release/billing-api/2.4 origin/main
git push -u origin release/billing-api/2.4
git branch -r --list 'origin/release/*' | sed 's#origin/release/##'
# billing-api/2.4
# web/2026.10
One shared release branch against per-product branchesA single repository-wide release branch ties every product to one cadence and makes every team's fixes compete for the same branch. Per-product release branches let each product stabilise and patch on its own schedule, with rules scoped to that product's paths.release/2.4 (shared)release/<product>/<ver>cadenceone for allper productwho cuts itrelease managerowning teamwhat may changeanythingproduct + its libsnumber of branchesoneone per active productmost monorepos need per-product branches only for products that actually stabilise One shared release branch against per-product branchesA single repository-wide release branch ties every product to one cadence and makes every team's fixes compete for the same branch. Per-product release branches let each product stabilise and patch on its own schedule, with rules scoped to that product's paths.release/2.4 (shared)release/<product>/<ver>cadenceone for allper productwho cuts itrelease managerowning teamwhat may changeanythingproduct + its libsnumber of branchesoneone per active productmost monorepos need per-product branches only for products that actually stabilise

Step 2 — Limit which paths a release branch may change Jump to heading

A product’s release branch should change only that product and the shared libraries it depends on. A required check compares the files changed in a pull request against an allow-list derived from the product’s dependency set.

#!/bin/sh
# ci/release-scope.sh <product> — fail if a PR to release/<product>/* touches other paths
set -eu
product=$1
allowed=$(python3 ci/deps_for.py "$product")      # e.g. services/billing libs/money libs/auth-client
base=$(git merge-base "origin/$BASE_REF" HEAD)
outside=$(git diff --name-only "$base" HEAD | while read -r f; do
  ok=no; for a in $allowed; do case "$f" in "$a"/*) ok=yes ;; esac; done
  [ "$ok" = yes ] || echo "$f"
done)
[ -z "$outside" ] || { echo "Paths outside $product's scope:"; echo "$outside"; exit 1; }

The dependency set comes from the same graph used in detecting affected projects from git diff.

Step 3 — Backport fixes with path awareness Jump to heading

Fixes land on main first and are cherry-picked to the product’s release branch. A fix commit on main sometimes touches several products at once; cherry-pick only what the release branch needs, or split the fix on main so each product’s part is its own commit.

# Cherry-pick only the billing part of a multi-product fix
git switch release/billing-api/2.4
git cherry-pick -x --no-commit abc1234
git restore --staged --worktree -- apps/ services/export/     # drop paths outside this product
git commit -C abc1234 --trailer "Backport-Scope: billing-api"
A fix reaching one product's release branchA fix on main touches the billing service, the shared money library and the web app. The billing release branch receives only the billing and money parts of it, cherry-picked and recorded with a trailer, while the web app's part stays on main for the web app's next release.only the product's paths travel to its release branchmainm1fix (3 paths)m2release/billing-api/2.42.4.0fix' (2 paths)2.4.1splitting multi-product fixes on main makes backports clean cherry-picks A fix reaching one product's release branchA fix on main touches the billing service, the shared money library and the web app. The billing release branch receives only the billing and money parts of it, cherry-picked and recorded with a trailer, while the web app's part stays on main for the web app's next release.only the product's paths travel to its release branchmainm1fix (3 paths)m2release/billing-api/2.42.4.0fix' (2 paths)2.4.1splitting multi-product fixes on main makes backports clean cherry-picks

Labelled backports work here too, with one label per product release branch, as in automating backport pull requests with labels.

Step 4 — Decide the rule for shared libraries Jump to heading

The hard question in a monorepo is what happens when a shared library needs a fix on a release branch. Two rules work; mixing them does not.

  • Libraries follow the product. The release branch carries its own copy of the library’s state, and library fixes are backported per product. Simple, and each product’s release is self-contained.
  • Libraries are released separately. Libraries have their own versions and tags, products pin a version, and release branches update the pin rather than the library code.
# Rule 1: library fix backported into one product's release branch
git cherry-pick -x <money-lib-fix>
# Rule 2: bump the pinned library version on the release branch instead
sed -i 's/"@acme\/money": "1.8.2"/"@acme\/money": "1.8.3"/' services/billing/package.json

Step 5 — Protect release branches per product Jump to heading

Protection rules target the naming pattern, with owners per product. Each team controls its own release branches; nobody can push to another product’s.

# CODEOWNERS on release branches
/services/billing/   @acme/billing
/libs/money/         @acme/billing @acme/payments-platform
# Ruleset: release/billing-api/* — PR required, billing team approval, no force-push
gh api "repos/$OWNER/$REPO/rulesets" --jq '.[] | select(.name|startswith("release-")) | .name'

Step 6 — Retire release branches when products move on Jump to heading

Products that stop supporting a version lock its branch. In a monorepo, stale release branches are particularly costly, because tooling scanning all branches — affected detection, security scanners — keeps processing them.

Does this product need release branches?If the product deploys continuously from main, it needs no release branches. If it stabilises before each release, it cuts short-lived release branches per version. If it supports several versions for customers, it keeps long-lived release branches within a published support window.How does this product ship?continuous from mainNo release branchestags onlystabilise then shipShort-lived branchesrelease/<p>/<v>several versions supportedLong-lived branchessupport windowdecide per product — the repository does not force one answer Does this product need release branches?If the product deploys continuously from main, it needs no release branches. If it stabilises before each release, it cuts short-lived release branches per version. If it supports several versions for customers, it keeps long-lived release branches within a published support window.How does this product ship?continuous from mainNo release branchestags onlystabilise then shipShort-lived branchesrelease/<p>/<v>several versions supportedLong-lived branchessupport windowdecide per product — the repository does not force one answer

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Should the whole monorepo ever have one release branch? Jump to heading

When all products ship together as one release — an installable suite, for example — a single release branch is the honest model. The per-product approach is for repositories whose products have independent lifecycles.

How do tags work with per-product branches? Jump to heading

Tags include the product too, such as [email protected], so they are unambiguous across the repository; see the tagging guide for conventions.

What about CI on release branches? Jump to heading

Run only the product’s affected set, computed against the release branch’s own base, plus a full build of that product. Running the whole monorepo’s suite on every release branch wastes time on code that cannot have changed.