Skip to content

Maintenance Branches

After a new major ships, the previous one usually still has users. Maintenance branches keep those lines releasable: when a release crosses a major boundary, a branch is created from the previous release — and fixes pushed to it go through the same pull request flow and release from that branch, without disturbing the new major.

The full lifecycle on an example:

  1. The project is released as 1.1.0. A breaking change lands and the release pull request ships 2.0.0 — the action creates the v1 branch from the v1.1.0 tag.
  2. A fix: ... commit is pushed to v1 — the action opens a release pull request targeting v1 with the 1.1.1 bump.
  3. The pull request is squash-merged — 1.1.1 is tagged and published from the maintenance branch:
    • the package goes to the release-1.x npm dist-tag, latest stays on 2.0.0;
    • the GitHub release is created without the Latest mark.

Enable maintenance branch creation in the config:

.simple-release.json
{
"project": ["@simple-release/pnpm#PnpmWorkspacesProject", {
"mode": "fixed"
}],
"maintenanceBranch": {
"enabled": true
}
}

And let the release workflow react to pushes to the maintenance branches:

.github/workflows/release.yml
name: Release
on:
issue_comment:
types: [created, deleted]
push:
branches:
- main
- 'v*'

For an independent monorepo the branches are named per package — pkg-name@1 — so use the '*@*' pattern instead of 'v*'.

Instead of the config, creation can also be enabled per run with the maintenance-branch action input.

The branch is created by the release workflow when the released version crosses a major boundary — for example 1.1.02.0.0, but not 2.0.02.1.0. It is created from the previous release tag and named after the previous major with the tag prefix: v1 from v1.1.0. In independent monorepos each package gets its own branch with its tag prefix: pkg-name@1 from pkg-name@1.1.0 — only for packages that actually crossed a major.

An existing branch is never overwritten — recreate it deliberately with the force option if needed.

OptionDescription
enabledEnable maintenance branch creation. Defaults to false.
forceRecreate the branch if it already exists.
branchPrefixPrefix for the branch name, when it should differ from the tag prefix. Defaults to the tag prefix.

Nothing special to learn: the flow on a maintenance branch is the same as on main. Push conventional commits, get a release pull request targeting the branch, squash-merge it — the release runs from the branch with the right base version.

What differs is the “latest” handling, automatically:

  • the npm package is published under the release-N.x dist-tag of its major — latest keeps pointing at the newest line;
  • the GitHub release is not marked as Latest.

Several maintenance lines can live side by side — v1 and v2 while main is on 3.x — each releasing independently under its own release-1.x and release-2.x tags.