Handbooks / CI/CD & DevOps / Chapter 3
Branching Strategies
7 pages · ~5 min✓ Reviewed
Builds on Testing in the Pipeline. Next up: Docker Images as Build Artifacts.
Trunk-Based Development: The Mental Model
1. What It Is
- Everyone integrates into one main branch (trunk) at least daily
- Trunk is always releasable
- Core metric: integration frequency
- Branches live hours to ~2 days, never weeks
2. Merging Is Not Releasing
- Small commit
- Merge to trunkflag off
- Deploydark
- Flag onrelease
3. Trunk-Based vs GitFlow
| Trunk-based | GitFlow | |
|---|---|---|
| Long-lived branches | 1 (trunk) | main, develop, release, hotfix |
| Feature branches | hours to ~2 days | until finished |
| Unfinished work | behind flags | held on branch |
- Needs fast automated tests on every commit
- Broken trunk: fix or revert first, before anything else
Merging never means releasing: feature flags keep unfinished work dark, and a branch left for weeks defeats trunk-based.
Short-Lived Feature Branches in Practice
1. The Daily Cycle
- Cycle: branch from fresh main, commit small, open PR, merge, delete branch same day
- Keep PRs 200-400 lines: small diffs review fast and conflict rarely
- Rebase before merging: keeps history linear
git switch main && git pull git switch -c feat/x git fetch origin && git rebase origin/main
start fresh, then sync before the PR merges
2. Merging
| Squash merge | Result |
|---|---|
| Trunk history | one commit per change |
| Undo a change | single git revert <sha> |
3. Big Refactors
- Branch by abstraction or a feature flag splits a refactor into steps
- Each step merges safely and ships dark
- Add abstraction
- Move callers
- New impl behind flag
- Remove old
GitFlow and Why It Slows CI
1. The Five Branch Types
| Branch | Role |
|---|---|
main | releases only |
develop | integration |
feature/* | one per feature, off develop |
release/* | stabilize before shipping |
hotfix/* | urgent fix, off main |
2. Why CI Slows Down
- feature/*merges when finished
- developlate integration
- release/*stabilization
- maintag and ship
- Features merge late, so integration is delayed and conflicts pile up
- CI runs on every long-lived branch: more pipeline runs, duplicate builds
- Stabilization gate: commit to production takes days or weeks
Merge into both main and develop. Forget one and the bug returns.
3. Fit and Interview Point
Release Branches, Merge Queues and Tagging
1. Release branches
release/1.4is cut from trunk only to support old versions- Fix on trunk first, then cherry-pick to the release branch
-xappends the original sha to the commit message
git switch release/1.4
git cherry-pick -x <sha>2. Merge queue
- PR queued
- Rebuilt on PRs ahead
- CI runs
- Merge to main
- Stops a semantic merge conflict from breaking main
- Enable it in branch protection
- CI must also trigger on the
merge_groupevent
3. SemVer and tags
| Change | Bump | 1.2.3 becomes |
|---|---|---|
| Breaking | MAJOR | 2.0.0 (lower reset) |
| Compatible feature | MINOR | 1.3.0 |
| Bug fix | PATCH |
Monorepo Pipelines with Path Filters
1. Path filters
- Pipeline runs only when matching files change
- Add shared deps:
libs/, lockfile, the workflow file itself - Else dependents skip tests they need
paths-ignore: ['docs/**', '*.md']skips CI for docs-only changes
on:
push:
paths:
- 'services/api/**'
- 'libs/shared/**'
- 'package-lock.json'
- '.github/workflows/api.yml'2. Required checks deadlock
- Required check + path filter: skipped workflow leaves the check pending
- PR can never merge
- Detect jobalways runs
- dorny/paths-filterwhat changed?
- Gated jobsonly if changed
3. Per-service tags
- Tags like
api-v1.2.0let each service release independently
on:
push:
tags: ['api-v*']deploy trigger for the api service
fetch-depth: 1 breaks change detection against the base branch. Fetch the diff range (fetch-depth: 0).
Cheat Sheet: Branching, Release and CI
1. Trunk Daily
git switch -c feat/x git fetch && git rebase origin/main # squash merge the PR, then: git push origin --delete feat/x
branch, sync, squash merge, delete
- Branch lifetime under 2 days
- PR under ~400 lines
- Trunk always green
- Trunk-based for continuous deploy
- GitFlow only for versioned, multi-release products
2. Release and SemVer
git tag -a v1.4.0 -m "Release 1.4.0" git push origin v1.4.0 git cherry-pick -x <sha> # on release/1.4
fix on trunk first, then cherry-pick
| Bump | When |
|---|---|
| MAJOR | breaking change |
| MINOR | compatible feature |
| PATCH | bug fix |
Never reuse or move a published tag. Ship a new patch version.
3. Monorepo CI
Check yourself
A bug is found in release/1.4. A dev commits the fix only on release/1.4. What goes wrong?
- Trunk never gets the fix, so 1.5 ships with the bug again.
- Fix on trunk first, then
git cherry-pick -x <sha>onto the release branch.
A docs-only PR is stuck on "Expected: waiting for status". The CI workflow uses paths: ['services/api/**'] and is a required check. Why, and what fixes it?
- The workflow was skipped, so the required check stays pending forever.
- Add an always-run change-detect job (dorny/paths-filter) that gates the downstream jobs.
Current version is 1.4.2. The next release has a bug fix and one breaking API change. What is the tag?
v2.0.0: a breaking change bumps MAJOR and resets MINOR and PATCH to 0.- Push it with
git push origin v2.0.0; tags are not pushed by default.