Learn Semantic Release - Multi-branch Release Strategy
Episode 15 of 23

Learn Semantic Release - Multi-branch Release Strategy

Managing a multi-branch release strategy with the rc branch for release candidates and main for stable, understanding SemVer precedence on the rc suffix, and putting together a release.config.cjs with several branch entries at once.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

In episode 14 the automatic changelog was running. The next question: how does a team preparing a big release still ship small fixes to production? The answer is multi-branch release — one repository, several release channels with different versions.

This episode covers the difference between prerelease, release candidate, and stable release; how the -rc.N suffix is ordered per SemVer; and the release.config.cjs and GitHub Actions configuration for the main plus staging branches.

Prerelease, Release Candidate, and Stable Release

TermVersion formatPurpose
Prerelease1.4.0-alpha.1, 1.4.0-beta.1Internal testing, features not yet complete
Release candidate1.4.0-rc.1Features complete, awaiting final verification
Stable release1.4.0Official release for users

Order of maturity: alpha is the rawest, beta is more mature, rc is ready to ship. Each can live on a separate branch and be published to a different channel.

SemVer Precedence for -rc.N

SemVer orders versions not alphabetically, but by the MAJOR.MINOR.PATCH components followed by prerelease. Watch this pattern:

SemVer order from rc to stable
1.4.0-rc.1 < 1.4.0-rc.2 < 1.4.0-rc.3 < 1.4.0

The larger the number after rc., the higher the precedence — but always below the same stable version. That means 1.4.0-rc.9 is still lower than 1.4.0. This is what lets automatic version comparison (e.g. npm install) know that rc is not a substitute for stable.

Additional rules: build metadata like +build.5 doesn't affect precedence. And when two prereleases are compared, the order follows the identifier order: alpha < beta < rc.

Multi-branch Configuration in release.config.cjs

Semantic-release reads the branch list from the branches key. Each entry can be a simple string or an object with channel and prerelease options:

release.config.cjs with main, staging, beta
module.exports = {
  branches: [
    { name: 'main' },
    { name: 'staging', channel: 'rc', prerelease: 'rc' },
    { name: 'beta', prerelease: 'beta' },
  ],
  tagFormat: 'v${version}',
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/npm',
    '@semantic-release/github',
  ],
};

What happens behind the scenes:

  • Push to main → release 1.4.0 (stable).
  • Push to staging → release 1.4.0-rc.1, then -rc.2, and so on.
  • Push to beta → release 1.4.0-beta.1 on the beta channel.
  • channel determines the dist-tag when publishing to npm and the GitHub Release target branch.
  • The default tagFormat value produces tags like v1.4.0-rc.1.

When staging is finally merged to main, semantic-release recomputes from the highest tag and produces the stable 1.4.0. Always verify new configuration with npx semantic-release --dry-run --no-ci before letting the workflow run automatically.

Warning

There can only be one branch per prerelease value. If two branches both use prerelease: 'rc', they'll fight over the rc.N numbers and the numbers can overwrite each other. For several parallel releases, use different prerelease names like alpha, beta, and rc.

Managing the Multi-branch Workflow in GitHub Actions

The release workflow simply needs to be triggered by pushes to both branches; semantic-release itself decides the release type based on the active branch:

.github/workflows/release.yml
name: Release
 
on:
  push:
    branches: [main, staging]
 
permissions:
  contents: write
 
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

No extra conditions are needed to choose the release type — the branches configuration in release.config.cjs is already the single source of truth. If explicit filtering is ever needed, the workflow could compare github.ref against refs/heads/main or refs/heads/staging, but for this case study let semantic-release handle it.

Tip

When two release branches run side by side, watch out for overlapping tags. Make sure the promotion flow is sequential: features are merged to staging, verified, then merged to main. Parallel releases from two branches with the same version base produce commit history that's complicated to order.

Conclusion

Episode 15 recap:

  • Prerelease (alpha/beta) → release candidate (rc) → stable, with different maturity levels.
  • SemVer: 1.4.0-rc.N is always lower than 1.4.0, and rc.1 < rc.2 < ....
  • branches in release.config.cjs supports multiple entries with channel and prerelease.
  • One branch per prerelease value so numbers don't collide.
  • The GitHub Actions workflow just needs triggers on pushes to main and staging; the release type is decided by configuration.

Multi-branch gives a team two release tracks at once. In episode 16 we'll touch Custom Release Plugins & Extensions — writing your own semantic-release plugin for organizational needs, using @semantic-release/exec, and integrating artifact repositories or private registries. See you there!

Learn Semantic Release - Multi-branch Release Strategy | Learn Semantic Release