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.

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.
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:
staging comes v1.2.0-rc.1 — with an rc suffix marking it as a candidate.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.
┌──────────────┐ merge ┌──────────────┐
│ staging │──────────►│ main │
│ (prerelease) │ │ (stable) │
└──────────────┘ └──────────────┘
│ │
▼ ▼
v1.2.0-rc.1 v1.2.0-rc.2 v1.2.0 v1.3.0Features 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.
| Branch | Example version | Nature |
|---|---|---|
main | v1.2.0 | Stable, production-ready |
staging | v1.2.0-rc.1 | Release candidate, pre-production |
feature/* | not released | Development |
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.
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.
npx semantic-release --dry-run --no-ci
npx semantic-releaseThe --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.
| Aspect | Dry run | Actual release |
|---|---|---|
| Compute next version | yes | yes |
| Assemble changelog | yes | yes |
| Create git tag | no | yes |
| Publish to npm | no | yes |
| Create GitHub release | no | yes |
| Commit CHANGELOG.md | no | yes |
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.
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-releaseNotice: 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.
| Mistake | Symptom | Solution |
|---|---|---|
| Manual tag push | Versions collide with SemVer results | Let semantic-release create the tags |
prerelease not configured | Staging release uses a stable version | Register the branch with prerelease |
Branch not in branches | No release on push | Add the branch to the configuration |
| Workflow trigger too broad | Release happens from another branch | Restrict branches on the push event |
In episode 7 you:
main produces stable versions without a suffix and staging/rc produce release candidates ending in rc.branches configuration serve two tracks without duplicating the workflow.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!