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.

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.
| Branch | Role |
|---|---|
main | Stable, only contains already-released code |
develop | Feature 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.
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:
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.
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:
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.
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 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.0Notice 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.
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:
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 stagingWith 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.
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.
In episode 4 you learned:
main, develop, feature/, release/, and hotfix/.staging (rc) for release candidates and main for stable.rc suffix produces the prerelease 1.2.0-rc.1 before the stable 1.2.0.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!