Learn Semantic Release - Staging vs Production Release Flow
Episode 7 of 23

Learn Semantic Release - Staging vs Production Release Flow

Dissecting the two-track release flow: staging as the home of release candidates ending in rc and main as the stable branch without a suffix. Including a comparison of dry run versus actual release and a workflow that serves both tracks at once.

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

Introduction

In episode 6 the CI workflow is up: the ci job validates every push and pull request, and the release job only runs on the main and staging branches. Now the question: if both branches trigger the same workflow, are the release results also the same?

The answer is no. A release from staging and a release from main are two different things: the former is a candidate, the latter a final product. This episode dissects the staging vs production release flow — how one semantic-release configuration serves two tracks, the difference between a dry run and an actual release, and why this pattern saves you from broken releases in production.

Main Discussion

Two Tracks, One Pipeline

Think of a restaurant: staging is the test kitchen where new menu items are tasted, main is the menu served to customers. It makes no sense to serve a dish that hasn't been tested. In release terms, staging holds release candidates — near-final versions ready to be tested in pre-production — while main only accepts stable versions.

The distinction between the two is visible in the version number:

  • From staging comes v1.2.0-rc.1 — with an rc suffix marking it as a candidate.
  • From main comes v1.2.0 — the full version, without any suffix.

The rc suffix isn't just cosmetic. According to SemVer rules, 1.2.0-rc.1 has lower precedence than 1.2.0, and the ordering 1.2.0-rc.1 < 1.2.0-rc.2 < 1.2.0 applies. A candidate is never considered to overwrite a stable version, and the ordering among candidates stays consistent.

The Release Flow in One Picture

Two-track release flow
┌──────────────┐   merge   ┌──────────────┐
│   staging    │──────────►│     main     │
│ (prerelease) │           │   (stable)   │
└──────────────┘           └──────────────┘
       │                         │
       ▼                         ▼
v1.2.0-rc.1   v1.2.0-rc.2    v1.2.0   v1.3.0

Features are developed on feature branches, merged to staging, then produce rcs that the QA team tests. Once approved, staging is merged to main and semantic-release computes the next stable version from all commits since the last stable tag.

Different Results on Each Branch

BranchExample versionNature
mainv1.2.0Stable, production-ready
stagingv1.2.0-rc.1Release candidate, pre-production
feature/*not releasedDevelopment

feature/* branches don't need to be registered — semantic-release only processes branches listed in the branches configuration. A push to any other branch is not considered a release moment and produces no version at all.

Dry Run vs Actual Release

A dry run is a rehearsal: the whole pipeline runs, the version is computed, the changelog is assembled — but without side effects. An actual release does everything to completion.

Dry run vs actual release
npx semantic-release --dry-run --no-ci
npx semantic-release

The --no-ci flag is needed when running on a local machine because semantic-release refuses to run outside a CI environment. The dry run is the best verification tool before a real release — covered in more depth in episode 10.

AspectDry runActual release
Compute next versionyesyes
Assemble changelogyesyes
Create git tagnoyes
Publish to npmnoyes
Create GitHub releasenoyes
Commit CHANGELOG.mdnoyes

Branch Configuration in release.config.cjs

release.config.cjs
module.exports = {
  branches: [
    "main",
    { name: "staging", prerelease: "rc", channel: "rc" },
    { name: "rc", prerelease: "rc", channel: "rc" },
  ],
  tagFormat: "v${version}",
};

"main" means stable with no suffix. Lines with prerelease: "rc" mean every release from that branch gets an rc suffix with a running number — the first commit produces 1.2.0-rc.1, the next 1.2.0-rc.2. The channel: "rc" option marks the npm package with a separate dist-tag so stable-version consumers don't get pulled into candidates.

Two-Track Release Workflow

Release workflow for main and staging
name: Release
on:
  push:
    branches: [main, staging]
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - name: Semantic Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

Notice: one workflow, two kinds of results. From staging comes v1.2.0-rc.1, from main comes v1.2.0. All the decisions live in the branches configuration, not in the workflow — one source of truth for release logic.

Tip

Never create version tags manually after the workflow has run. A manual tag will be treated as the latest release by semantic-release and can prevent the next version from ever shipping. Let the tool create the tags, and let the workflow manage the entire cycle.

Common Mistakes

MistakeSymptomSolution
Manual tag pushVersions collide with SemVer resultsLet semantic-release create the tags
prerelease not configuredStaging release uses a stable versionRegister the branch with prerelease
Branch not in branchesNo release on pushAdd the branch to the configuration
Workflow trigger too broadRelease happens from another branchRestrict branches on the push event

Conclusion

In episode 7 you:

  • Understood that main produces stable versions without a suffix and staging/rc produce release candidates ending in rc.
  • Saw one branches configuration serve two tracks without duplicating the workflow.
  • Distinguished a dry run (no side effects) from an actual release (creates tags, publishes, and creates GitHub releases).
  • Realized that an rc prerelease never overrides a stable version per SemVer rules.

In episode 8 we'll take apart Commit Analyzer & Release Rules: how to determine which commit types bump major, minor, or patch, and how to customize the rules for your team's needs. See you in episode 8!

Learn Semantic Release - Staging vs Production Release Flow | Learn Semantic Release