Maintaining LTS Branches with Backport Releases
After a major release, many users cannot upgrade immediately: the migration takes work, their framework version lags behind, or their own release cycle is slow. A long-term support (LTS) line — a maintenance branch that keeps receiving security and critical fixes for the previous major — lets them stay safe while they plan the upgrade. It also creates new ways to break releases, and each of them tends to surface at the worst moment — during an urgent security fix: backports that move latest backwards, fixes that land on one line and not the other, and maintenance branches that silently rot. This guide sets up a maintenance branch, backports fixes with cherry-picks, publishes LTS releases under their own dist-tag with Changesets or semantic-release, and defines a support policy that users can rely on.
The shape of an LTS line
After 4.0.0 ships from main, the last 3.x commit becomes the base of a v3.x branch. Fixes land on main first and are cherry-picked to v3.x when they qualify; each backport release (3.8.5, 3.8.6, ...) is published under a v3-lts dist-tag while latest stays on 4.x. The general channel model is covered in Release Channels and Dist-Tags.
Setting up the maintenance branch
# Branch from the last 3.x release tag
git switch -c v3.x v3.8.4
git push -u origin v3.x
# Label the current 3.x release so users have a stable pointer
npm dist-tag add your-lib@3.8.4 v3-lts
Protect the branch like main: required reviews, required status checks, and a CI configuration that still works for the old code. The last point is easy to miss — CI images, Node.js versions and tooling move on, and an LTS branch whose pipeline no longer runs cannot ship the security fix it exists for. Pin the maintenance branch's CI to the toolchain it was released with, and run its pipeline on a schedule so breakage is found before a fix is urgent.
Backporting a fix
Fix forward first: land the fix on main, then backport. That order guarantees the current major never lacks a fix that an older line has.
git switch v3.x
git switch -c backport/sanitize-input-v3
git cherry-pick -x <commit-sha-from-main> # -x records the original commit in the message
# resolve conflicts if the code diverged, then run the tests
git push -u origin backport/sanitize-input-v3
# open a pull request into v3.x
The -x flag appends "(cherry picked from commit ...)" to the message, which makes it easy to trace a backport to its original. Many teams automate the cherry-pick with a bot: a backport v3.x label on the merged pull request opens the backport pull request automatically, and flags conflicts for a human.
Publishing LTS releases under their own tag
The critical rule: an LTS release must never be published to latest, or latest moves backwards to 3.x for every new install — the failure described in Fixing a Wrong latest Dist-Tag.
With semantic-release, maintenance branches are first-class. A branch named like 3.x is recognised as a maintenance branch with a version range and publishes to a channel you define:
{
"branches": [
{ "name": "3.x", "range": "3.x", "channel": "v3-lts" },
"main",
{ "name": "next", "prerelease": "rc", "channel": "next" }
]
}
semantic-release refuses to publish versions outside the branch's range, so a backport can never produce 4.x from the maintenance branch.
With Changesets, run the normal version and publish flow on the maintenance branch, but pass the tag explicitly:
pnpm changeset version
pnpm changeset publish --tag v3-lts
and add a CI guard so the maintenance branch cannot publish without it:
- name: Publish LTS
if: github.ref_name == 'v3.x'
run: pnpm changeset publish --tag v3-lts
- name: Publish latest
if: github.ref_name == 'main'
run: pnpm changeset publish
Changelogs and version bookkeeping
Two release lines means two streams of changelog entries, and they must not contradict each other. With Changesets, each branch keeps its own CHANGELOG.md history from the point it diverged: entries for 4.x releases on main, entries for 3.x backports on v3.x. When a backport pull request carries the original changeset file, Changesets consumes it on the maintenance branch and writes a 3.x entry, while the same fix already appears in the 4.x changelog — both are correct, because both lines released it. With semantic-release, notes are generated per release from commit messages, and the -x trailer in cherry-picked commits links each backport to its original.
Keep one place where users can see a fix's status across lines. For security fixes this is the advisory, which lists every fixed version (5.3.1 and 4.12.7); for other fixes, a line in each release's notes such as "Backport of #812 from 5.3.1" is enough.
Version bookkeeping has one more trap: merging a maintenance branch back into main. It is rarely needed — fixes already came from main — and merging would bring 3.x versions and changelog entries into the 4.x line. Do not merge maintenance branches forward; if a fix was developed on the maintenance branch first under time pressure, cherry-pick it forward to main instead.
Tooling for backports
Several tools automate the mechanical part. Backport bots and GitHub Actions (for example, actions that read a backport <branch> label on a merged pull request) create a branch from the maintenance branch, cherry-pick the merged commits and open a pull request, commenting on the original pull request when a conflict needs manual work. The same pattern works on GitLab with merge-request labels and a small CI job. Automation matters less for speed than for completeness: a labelled pull request that failed to backport stays visible, while a manual process quietly forgets fixes. Pair the bot with a periodic report of pull requests labelled for backport but not yet released on the maintenance line.
Support policy
An LTS line is a promise, so write it down in the README and the release notes of the new major:
- What is supported: "3.x receives security fixes and fixes for critical bugs."
- For how long: a date, such as "until 2027-03-31", or a rule such as "for 12 months after 4.0.0".
- What is not: "No new features. Minor bugs are fixed on 4.x only."
- How to upgrade: a link to the migration guide.
When the window ends, publish a final release if one is pending, deprecate the line with a message pointing to the upgrade guide, and archive the branch. Deprecation is covered in Deprecating npm Package Versions.
Worked example: a security fix across two lines
A vulnerability report arrives for input sanitisation in a form library with latest on 5.x and an LTS line for 4.x. The fix merges to main and ships as 5.3.1. The backport bot opens a pull request into v4.x; the cherry-pick conflicts because a helper was renamed in 5.x, and a maintainer adapts it in a few lines. The maintenance pipeline — run weekly, so it still works — passes, and semantic-release publishes 4.12.7 to the v4-lts channel. npm view form-lib dist-tags shows latest: 5.3.1 and v4-lts: 4.12.7, and the security advisory lists both fixed versions.
Prevention and guardrails
- Fix forward on
mainfirst, then backport. - Enforce the LTS tag in CI, or use semantic-release's maintenance branches.
- Keep the maintenance pipeline green with scheduled runs on a pinned toolchain.
- Publish a support window and retire the line when it ends.
Frequently Asked Questions
How many LTS lines should we maintain? Usually one — the previous major. Each additional line multiplies backport and CI work. Large ecosystems with enterprise users sometimes maintain two, with a clear end date for the older one.
Should consumers install the LTS tag?
Most do not need to: users on 3.x with ^3.8.0 ranges receive backport releases automatically. The tag helps new installs that must stay on 3.x (npm install your-lib@v3-lts) and documents which release is current for the line.
What if a fix cannot be backported cleanly? Write a targeted fix for the old line, reviewed separately, and link it to the original. If the risk is high, document the limitation and recommend upgrading.
Do LTS releases need the same testing as main? Yes, for the paths they touch. Backports are small, but the code around them is older and the toolchain differs, so run the maintenance branch's full test suite and a tarball smoke test before publishing.
Related
- Release Channels and Dist-Tags explains how LTS tags relate to other channels.
- Fixing a Wrong latest Dist-Tag recovers from a backport published to
latest. - Configuring Conventional Commits and semantic-release sets up the release tool used for maintenance branches.
- Package Deprecation and Lifecycle covers ending support for a line.