main?main?release/vX.X and hotfix/vX.X.X branches?develop branch?main while a release branch is being hardened?Pure Trunk-Based Development (TBD) assumes main is always deployable — every commit can go to production. That works beautifully for SaaS products with Continuous Deployment. But many teams need a hardening phase (QA, UAT, compliance sign-off) before shipping. They need a gate, but they do not want the heavyweight overhead of GitFlow’s permanent develop branch and its “Merge Hell.”
Conversely, GitFlow’s long-lived feature branches and dual main/develop structure create friction with CI/CD pipelines — as Atlassian’s own documentation acknowledges, GitFlow can be challenging to use with CI/CD because its fundamental structure prioritizes scheduled releases over continuous delivery.
main is the only permanent branch. There is no develop branch.main via Pull Requests with squash or regular merge.release/vX.X branch is cut from main. This branch is used exclusively for hardening — bug fixes, documentation, version bumps. No new features enter it.main to capture any fixes made during hardening.In one sentence: Develop on the trunk, stabilize on a release branch, deploy from the release branch.
| Aspect | Pure TBD | TBD + Release Branch | GitFlow |
|---|---|---|---|
| Permanent branches | main only | main only | main + develop |
| Feature branches | Hours (< 24h) | Hours (< 24h) | Days, weeks, months |
| Release mechanism | Tag on main, deploy immediately | Cut release/vX.X from main, harden, then deploy | Cut release/vX.X from develop, harden, merge to main |
| Where features merge to | main | main | develop |
| Hotfix source | Direct commit on main | Branch from release/vX.X or main | Branch from main |
| CI/CD compatibility | Excellent | Very Good | Challenging |
| QA/Hardening phase | None — trunk is always green | Yes — on the release branch | Yes — on the release branch |
| Merge complexity | Minimal | Low | High (“Merge Hell”) |
| Feature Flags needed? | Yes | Yes | No |
| Old version support | Not supported | Support branches from tags | Support branches from tags |
The Gemini chat in context highlights several reasons GitFlow is considered “anti-CI/CD”:
develop, feature, release, hotfix, main) create massive merge diffs at the end of a release cycle.release branch is created — days or weeks later.develop, the code must wait for a formal release window, a merge to release, and another merge to main.TBD + Release Branch eliminates most of these issues: features integrate into main continuously (satisfying CI), and the release branch is only a short-lived stabilization gate (not a long-lived development line).
main receives code multiple times a day. Your CI pipeline has one permanent target to test against — no juggling between develop and main.main is tested immediately against the latest state of the codebase. Bugs are caught within hours of being written.develop branch. Ideal for teams with QA gates or compliance requirements.develop branch, you eliminate the “syncing nightmare” — the need to constantly merge between main, develop, release, and hotfix branches.main is always the single source of truth for development. There is no ambiguity about which branch represents “the latest code.”main (new features). Blurring this line turns the release branch into a second trunk.develop branch) but need a safety net for production (the release branch).main. The release branch is unnecessary overhead.End-to-end examples using Mermaid diagrams, covering feature development, release cycles, hotfixes, and legacy version support.
Developers work on short-lived branches (hours, max 1–2 days). Incomplete features are hidden behind feature flags. All code merges to main — there is no develop branch.
Every feature lands on main via a Pull Request. The trunk is always the latest integrated state of the codebase.
When the team decides to ship, a release branch is cut from main. The release branch enters a hardening phase — only bug fixes, documentation updates, and version bumps are allowed. Meanwhile, main continues to receive new feature work.
After the release ships:
main to capture any bug fixes made during hardening.v2.0 is applied to the final commit on the release branch.Key difference from GitFlow: The release branch is cut from
main, not fromdevelop. There is nodevelopbranch. Features and release hardening both flow throughmainas their ultimate destination.
If a critical bug is found in production while the release branch is still active, the fix is applied on the release branch and then merged back into main.
If the release branch has already been deleted (the release shipped and the team moved on), the hotfix is committed directly to main (pure TBD style) or on a short-lived hotfix branch from main.
The hotfix branch is cut from the v2.0 tag (the exact code running in production), not from the tip of main (which has new features). The fix is merged into main and tagged.
If a client reports a bug in v1.0 while main is at v3.0, create a support branch from the old tag. This is identical to the GitFlow support branch mechanism.
If the bug also affects the current version, cherry-pick the fix into main:
Reading the diagram:
v1.0 gets a patch (v1.0.1) on its own isolated branch.feat/auth and feat/dashboard are short-lived branches that merge directly into main via PRs. No develop branch intermediary.release/v2.0 is cut from main. Only QA fixes go here. After hardening, it is tagged and deployed, then merged back into main.feat/reports) even while the release branch is being hardened.v2.0 is fixed on a hotfix branch and merged into main.Key takeaway: Code flows in one direction — from feature branches →
main→ release branch → production. The release branch is a short-lived “snapshot” ofmainthat gets stabilized, not a permanent development line.
mainFeatures merge into main throughout the sprint via short-lived branches. main is always the latest integrated state of the codebase. CI runs on every commit.
When the team decides the current state of main has enough features for a release, a release branch is created:
git checkout -b release/v2.0 main
main continues to receive new feature work for the next release.The release branch is deployed to a staging or UAT environment.
main so they are not lost.Once the release branch is signed off:
v2.0.0) is applied to the final commit on the release branch.The release branch is merged back into main:
git checkout main && git merge --no-ff release/v2.0
This ensures that any bug fixes, version bumps, or changelog edits made during hardening are captured in main’s history. Without this merge, those changes would be lost.
The release branch is deleted. It served its purpose — a temporary stabilization lane.
| Aspect | Pure TBD | TBD + Release Branch | GitFlow |
|---|---|---|---|
| Release Timing | Continuous (every commit) | Batched (sprint cadence) | Batched (sprint cadence) |
| Release Trigger | Tag on main | Cut release/vX.X from main | Cut release/vX.X from develop |
| QA Phase | None — automated tests only | On the release branch | On the release branch |
| Deploy Source | main (tagged commit) | Release branch (tagged commit) | main (after merge from release) |
| Branches Active During Release | 1 (main) | 2 (main + release branch) | 3+ (main + develop + release branch) |
| New Features During QA | Continue on main | Continue on main | Continue on develop |
| Hotfixes During QA | Direct commit on main | Fix on release branch | Fix on hotfix branch from main |
develop branch and moving to TBD + Release Branch is the lowest-risk step.If the bug is in the version that was just released and the release branch is already deleted:
git checkout -b hotfix/v2.0.1 v2.0.0main and tag as v2.0.1.main.If the release branch is still active (hardening is ongoing), the fix goes on the release branch instead — see Q&A #2.
If main has moved to v3.0 and a client needs a fix for v1.0:
git checkout -b support/v1.x v1.0.0v1.0.1 and deploy from the support branch.main if the bug also exists in the current version.| Scenario | Strategy | Branch From | Merge Into | Deploy From |
|---|---|---|---|---|
| Bug in the current live version (release branch active) | Bugfix on release branch | release/vX.X | Release branch + main | Release branch |
| Bug in the current live version (release branch deleted) | Hotfix branch | Release tag on main | main | main |
| Bug in a much older version | Support branch | Old version tag | support/vX.x only | support/vX.x |
| Bug found during QA (not live) | Fix on release branch | release/vX.X | Release branch | Not deployed yet |
git cherry-pick [commit-hash] to apply the exact fix without merging unrelated code.Because this model uses short-lived branches and a single trunk, conflicts are inherently less frequent than in GitFlow. But they can still happen — especially around the release branch.
The number-one conflict-prevention strategy is the same as pure TBD: merge early, merge often.
The release branch is the most conflict-prone area in this model. While you are hardening release/v2.0, new features keep landing on main.
main into the release branch periodically? — No. This defeats the purpose of a stable release branch. You would be pulling in untested features.main periodically? — Yes. Periodically merge bug fixes from the release branch back into main. This prevents a massive “sync back” merge at the end.# While release/v2.0 is being hardened
git checkout main && git merge release/v2.0
When your short-lived feature branch needs the latest main code:
git pull --rebase origin main
This replays your commits on top of the latest main, resolving conflicts one commit at a time instead of in a single giant diff.
.gitattributes and Pre-commit HooksPrevent “false conflicts” caused by formatting differences:
.gitattributes to enforce line endings (LF or CRLF).Open a Draft Pull Request as soon as you start a feature branch. Even if the code is not ready, teammates can see which files you are working on. Platforms like GitHub show conflict warnings on PRs in real-time.
| Action | Frequency | Purpose |
|---|---|---|
Merge feature branches to main | Multiple times daily | Keep the trunk current. |
Rebase feature branches on main | Before pushing | Linear history, small conflicts. |
Merge release branch fixes back to main | Every few days | Prevent “big bang” sync at release end. |
| Open Draft PRs | Immediately when starting | File-level conflict visibility. |
| Commits | Hourly | Small, logical “save points.” |
| Operation | What It Does | Golden Rule |
|---|---|---|
| Rebase | Replays your commits on top of the target branch’s latest state. Rewrites commit history. | Use on private/local branches only. Never rebase a shared branch. |
| Merge | Creates a new “merge commit” that ties two histories together. Preserves both histories. | Use when integrating into a shared/protected branch. |
The One Rule: Rebase down, Merge up. Pull changes down into your private branch with rebase. Push changes up into a shared branch with merge.
git pull --rebase origin mainmain, keeping a linear history and surfacing conflicts one commit at a time.main, keeping the trunk history clean and easy to bisect. A regular --no-ff merge is acceptable if you want to preserve granular commit history.git checkout -b release/v2.0 maingit checkout main && git merge release/v2.0main so they are not lost. A merge commit clearly records the sync point. Do this periodically — not just at the end.git checkout main && git merge --no-ff release/v2.0--no-ff merge commit acts as a permanent record of the sync point. This is the final “catch-all” merge that captures any remaining bug fixes, version bumps, or changelog edits.git checkout main && git merge --no-ff hotfix/v2.0.1git checkout release/v2.0 && git cherry-pick [commit-hash]main while a release branch is active, cherry-pick the specific fix commit into the release branch. A full merge from main would pull in new features that should not be in the release.git checkout support/v1.x && git cherry-pick [commit-hash]main have diverged too far for a merge. Cherry-pick applies only the exact fix commit.| Merge Point | Direction | Operation | Why |
|---|---|---|---|
| Update feature from main | ↓ into your branch | Rebase | Small conflicts, linear history |
| Feature → Main | ↑ into trunk | Squash Merge | Clean single-commit history |
| Release → Main (periodic sync) | ↓ backport fixes | Merge | Captures hardening fixes |
| Release → Main (final sync) | ↓ final backport | Merge (--no-ff) | Permanent sync record |
| Hotfix → Main | ↑ into trunk | Merge (--no-ff) | Visible hotfix record |
| Hotfix → Active Release Branch | ↓ isolated patch | Cherry-Pick | Avoids pulling in new features |
| Fix → Legacy Support Branch | ↓ isolated patch | Cherry-Pick | Avoids dragging in unrelated code |
A major framework upgrade (e.g., .NET 6 → 8, Angular 14 → 18) is challenging in any branching model. In TBD + Release Branch, the strategy leverages the strengths of the single-trunk model.
This is the most TBD-native approach. Break the upgrade into small, independently mergeable chunks. Each chunk is merged to main behind a feature flag.
main behind a feature flag so the old code path remains active.Benefits:
Drawback:
Wrap framework-specific features (HTTP clients, auth providers, state management) in your own interfaces. Upgrade the underlying implementation on feature branches while keeping the interface stable.
When the upgrade is too large for incremental migration (e.g., a complete rewrite of the build pipeline):
chore/framework-upgrademain into the upgrade branch daily. This is the critical discipline — you absorb new feature code and fix breaking changes as they arrive, rather than facing 1,000 errors on merge day.main briefly, merge the upgrade in, and have everyone rebase their active feature branches.Key difference from GitFlow: In GitFlow, you would merge
developinto the upgrade branch. Here, you mergemain— and sincemainis the only trunk, the merge surface is identical.
For massive rewrites (e.g., monolith to microservices):
| Strategy | Conflict Risk | Speed | Best For |
|---|---|---|---|
| Feature Flags + Incremental | 🟢 Low | Moderate | Standard upgrades with clear module boundaries |
| Branch by Abstraction | 🟢 Low | Slow | Deep framework API changes |
| Dual-Track (daily merge) | 🟡 Medium | Moderate | Large-scale build/tooling upgrades |
| Strangler Fig | 🟢 Low | Very Slow | Full rewrites, architecture migrations |
main?Create a new short-lived branch from main and treat the change as a new piece of work:
You do not reopen or continue the old branch. The old merge commit is already part of main’s history. Branch from main (which now contains your feature), make the fix, open a new PR, and merge.
If a release branch has already been cut and this feature is part of that release, the fix goes on the release branch:
If it has already shipped to production, it becomes a hotfix — branch from main (or the release tag), fix, merge back to main, and tag as a patch release.
Bug fix branches during QA are created from the release branch — not from main:
Key rules:
release/v2.0release/v2.0 (via PR, with review)main for release bugs — main already has new features for the next release that should not contaminate the hardening branch.release/v2.0 into main so bug fixes are not lost.A patch release (e.g., v2.0.1) is a targeted fix for a version already in production.
If main has moved ahead with new features since the release:
git checkout -b hotfix/v2.0.1 v2.0.0main and tag as v2.0.1.Each patch is an independent, short-lived branch. They can follow one after another — v2.0.1, v2.0.2, etc.
If main has moved to v3.0 but a client runs v2.0, use a support branch:
git checkout -b support/v2.x v2.0.0v2.0.1 and deploy from the support branch.main if the bug also exists in the current version.main. Every hotfix must reach main so it is not lost in the next release.The hotfix branch is never the deployment source. Deployment always happens from the receiving branch after the merge.
mainhotfix/v2.0.1 merges into main.v2.0.1 is applied to the merge commit on main.main.support/vX.xpatch/v2.0.1 merges into support/v2.x.v2.0.1 is applied on support/v2.x.support/v2.x.main for an older version?main has moved to v3.0. It contains code that did not exist in v2.0. Deploying from main would ship v3.0 code to a client expecting a v2.0 patch.
| Scenario | Hotfix merges into | Deployed from | Tag applied on |
|---|---|---|---|
| Bug in the current live version | main | main | main |
| Bug in an older version | support/vX.x | support/vX.x | support/vX.x |
main?Yes — the merge back into main is mandatory regardless of whether any bug fixes were made.
Even if zero bugs were found, the release branch may still differ from main:
main after the release branch was cut. The merge back creates a sync point that ties the two histories together.main. If the release branch was never merged back, main’s history diverges, making future operations messier.Key point: The merge back into
mainkeeps the histories aligned. It happens every time — zero fixes or fifty, it makes no difference to the process.
release/vX.X and hotfix/vX.X.X branches?They look similar — both lead to a deployable version — but they serve fundamentally different purposes.
| Aspect | release/v2.0 | hotfix/v2.0.1 |
|---|---|---|
| Purpose | Ship a planned set of features | Fix a critical bug in production |
| Branches from | main | Release tag (e.g., v2.0) or main |
| Contains | QA hardening fixes + version bumps | A single, surgical fix |
| Lifespan | Days to a week (QA cycle) | Hours to a day (emergency) |
| New features allowed? | No (feature freeze) | No |
| Merges into | main (sync back) | main |
| Version bump | Minor or Major (v2.0, v3.0) | Patch only (v2.0.1) |
| Trigger | Planned sprint/release cycle | Unplanned — production incident |
Think of it this way: A release branch is a planned departure — you pack your bags, go through security, and board the plane. A hotfix branch is an emergency landing — something went wrong mid-flight, and you fix it as fast as possible.
develop branch?The develop branch in GitFlow exists to serve as a “staging area” between feature branches and main. In TBD + Release Branch, main itself serves that purpose.
develop Does in GitFlow vs. How TBD + RB Handles ItGitFlow develop Purpose | TBD + RB Equivalent |
|---|---|
| Integration target for feature branches | main — features merge directly into the trunk |
| ”What will ship in the next release” | main — the release branch is cut from main when ready |
| Source for release branches | main — release branches are cut from main |
| Receives hotfix backports | main — hotfixes merge into main |
| CI/CD deploys to staging | main — CI deploys to staging from main; production deploys from the release branch tag |
develop?develop, then develop would need to be synced with main at release time. This is the exact “syncing nightmare” that makes GitFlow challenging with CI/CD.develop is one step further from production than code merged into main. This adds latency to the feedback loop.In GitFlow, develop is a “waiting room” where features sit until a release is cut. In TBD + Release Branch, features go directly to the “main hall” (main). When it is time to ship, a snapshot is taken (the release branch). The waiting room is unnecessary.
Key point:
mainin TBD + RB does the job of bothmainanddevelopin GitFlow. It is both the integration target and the source for releases. The release branch provides the stabilization gate thatdevelopnever could — becausedevelopis a permanent moving target, while a release branch is a frozen snapshot.
The version number follows Semantic Versioning (SemVer): MAJOR.MINOR.PATCH (e.g., v2.4.1).
| Version Part | Bumped When | Signal to Consumers | Example |
|---|---|---|---|
MAJOR (X.0.0) | Breaking changes — APIs, behaviors, or contracts change in non-backward-compatible ways | ”You will need to update your code” | v1.0.0 → v2.0.0 |
MINOR (0.X.0) | New features that are backward-compatible | ”New capabilities, your existing code still works” | v2.0.0 → v2.1.0 |
PATCH (0.0.X) | Bug fixes without new features or breaking changes | ”Something was broken, now it’s fixed” | v2.1.0 → v2.1.1 |
| Release Type | Branch | Source | Trigger |
|---|---|---|---|
| Major Release | release/v3.0 | Cut from main | Breaking changes, major rewrites |
| Minor Release | release/v2.1 | Cut from main | New features accumulated over a sprint |
| Patch Release | hotfix/v2.1.1 | Cut from release tag or main | Critical bug in production |
During QA hardening on a release branch:
| Tag | Meaning |
|---|---|
v2.1.0-alpha.1 | Early build, active development on the release branch |
v2.1.0-beta.1 | Feature-complete, undergoing QA |
v2.1.0-rc.1 | Release Candidate — believed final, pending sign-off |
v2.1.0 | Stable, production-ready |
Yes, but it is uncommon and adds complexity. In practice, most teams using TBD + Release Branch have at most two branches active at the same time:
release/v2.0 — currently being hardened.release/v2.1 — cut from main because the team moved fast and a new batch of features is ready while v2.0 is still in QA.release/v2.0 and release/v2.1, cherry-pick the fix into both branches.main first, then the newer one. This ensures main accumulates changes chronologically.v2.0 to ship before cutting v2.1.main while a release branch is being hardened?main continues to receive new feature work as normal. This is one of the biggest advantages of TBD + Release Branch over GitFlow.
In GitFlow, cutting a release branch from develop means develop is partially frozen — the team is split between QA work on the release branch and new feature work on develop, and the develop branch sometimes gets “stuck” waiting for the release to sync back.
In TBD + Release Branch:
main never stops. Features C, D, and E are merged while the release branch is being hardened.main. Any bug fixes made during hardening are captured. The new features on main are untouched.main?This is the only conflict risk in this model. When the release branch is merged back into main, a fix on the release branch may conflict with new code on main. The resolution is straightforward:
Key point:
mainis never blocked by the release process. Development velocity is maintained even during QA hardening. This is the primary advantage over GitFlow, where thedevelop→release→mainpipeline can create bottlenecks.
Presentation
A deep-dive into TBD + Release Branch — the hybrid Git branching model that combines trunk-based speed with release-grade stability. Covers workflows, release mechanics, hotfixes, conflict strategies, and Q&A.
April 20, 2026