Learn Semantic Release - Git Flow Branching Strategy
Episode 4 of 23

Learn Semantic Release - Git Flow Branching Strategy

Understanding the Git Flow branching strategy and its adaptation to two branches: staging as the release candidate with an rc suffix, and main as the stable release. Including a branch lifecycle diagram and the order of git commands from feature to release.

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

Introduction

In episode 3 we learned to write well-formed commits. But commits don't live alone — they're born on a branch, reviewed through a pull request, then merged into another branch. The branching strategy determines where those commits gather, and because semantic-release reads history per branch, it determines what version is born and where it's released.

Classic Git Flow has many branches: main, develop, feature/, release/, and hotfix/. However, many modern teams simplify it into two main lines that are easier to manage: staging for release candidates, and main for stable releases. This repo (devvnull.vercel.app) uses that two-branch pattern with an rc suffix on staging.

Main Discussion

The Classic Git Flow Concept

BranchRole
mainStable, only contains already-released code
developFeature integration before release
feature/*Feature development per task
release/*Release preparation: bugfixes + feature freeze
hotfix/*Emergency fixes straight from main

The idea: features are developed on feature/*, integrated into develop, then release/* freezes features and matures the release before it's merged to main. Unfortunately, more branches means more manual process that has to be agreed upon.

Modern Adaptation: main + staging

Instead of maintaining five branch types, many teams settle for two:

  • main — the stable branch. Every push triggers a full SemVer release (1.2.0).
  • staging — the prerelease branch. Every push triggers a candidate release (1.2.0-rc.1, 1.2.0-rc.2, etc.).

The branches configuration in this repo's release.config.cjs looks like this:

Branch configuration in release.config.cjs
branches: [
  "main",
  { name: "staging", prerelease: "rc" },
],

On main, release tags look like v1.2.0. On staging, the same version gets an rc suffix and a counter that increments on each release — 1.2.0-rc.1, 1.2.0-rc.2, and so on — until the team agrees to go stable.

Why Release Candidates?

Release candidates give the team a concrete target to test before users see it. QA, product owners, and testers pull version 1.2.0-rc.1 into the staging environment, test the new features, then approve or reject. All of that happens without ever touching main. Once approved, staging is merged into main, and semantic-release computes the stable 1.2.0 version from the same commits.

The version sequence produced from a single feature cycle usually looks like this:

Version evolution on staging then main
staging push #1  -> v1.2.0-rc.1   (feature lands, ready for testing)
staging push #2  -> v1.2.0-rc.2   (bugfix from review)
staging push #3  -> v1.2.0-rc.3   (final QA fixes)
main merge       -> v1.2.0        (stable, without the rc suffix)

Notice that the rc counter only increments on staging. Once the same commits are merged to main, the version becomes 1.2.0 without a suffix — because there's no prerelease configuration on main.

Tip

Use the rc (release candidate) suffix only for branches that truly are release candidates. If you need a looser development environment, give it a different suffix name so the version history doesn't get mixed up.

Branch Lifecycle

Branch lifecycle: feature -> staging (rc) -> main
feature/*           staging (rc)          main (stable)
     |                   |                     |
     |-- merge --------> |   release 1.2.0-rc.1 |
     |                   |                     |
     |                   |-- merge ----------> |  release 1.2.0
     |                   |                     |

This flow is linear and predictable: features land on staging, are tested as rc, then promoted to main. There's no commit leakage to main without passing through staging — except for emergency hotfixes that are deliberately routed around it.

Git Command Sequence

Command flow: feature -> staging -> main
git checkout -b feature/login
git add .
git commit -m "feat: add login page"
git push origin feature/login
# PR is reviewed, then merged to staging
git checkout staging
git merge feature/login
git push origin staging      # semantic-release releases 1.2.0-rc.1
# staging verification passes, then merged to main
git checkout main
git merge staging
git push origin main         # semantic-release releases 1.2.0

Notice that you never run semantic-release manually — every git push origin to a registered branch is enough to trigger the CI pipeline, and the pipeline runs the release. Git commands only orchestrate the flow of commits; versions, changelogs, and tags are the machine's responsibility.

Warning

Never force-push to a protected main or staging. Release tags point to specific commits; rewriting history makes those tags point to different history and the changelog will be recomputed incorrectly.

Hotfix Flow

Sometimes a production bug is too urgent to wait for the feature flow. A hotfix bypasses staging and goes straight to main, then is cherry-picked back to staging so both stay in sync:

Hotfix flow: straight to main, then sync to staging
git checkout -b hotfix/critical-bug
git commit -m "fix: handle crash while loading data"
git push origin hotfix/critical-bug
# PR is reviewed, merged directly to main
git checkout main
git merge hotfix/critical-bug
git push origin main              # semantic-release releases v1.2.1
git checkout staging
git cherry-pick main              # sync the fix to staging

With this pattern, an urgent fix ships quickly as a patch (v1.2.1) without waiting for a full feature cycle, while staging keeps the same code for ongoing development.

When to Delete a Branch?

feature/* branches are deleted after being merged. staging and main are kept for the lifetime of the project. Unlike classic Git Flow where release/* is discarded once finished, staging is permanent in the two-branch pattern — it's the promotion path to main, not a temporary container.

Conclusion

In episode 4 you learned:

  • Classic Git Flow with main, develop, feature/, release/, and hotfix/.
  • The two-branch adaptation: staging (rc) for release candidates and main for stable.
  • The rc suffix produces the prerelease 1.2.0-rc.1 before the stable 1.2.0.
  • The branch lifecycle and git command sequence from feature to release.

In episode 5 we'll install and configure semantic-release: install the core plugins, put together release.config.cjs with the main and staging branches, and set up custom commit analyzer rules. See you in episode 5!

Learn Semantic Release - Git Flow Branching Strategy | Learn Semantic Release