Branching Strategies & Trunk-Based Flow
Trunk-based flow keeps main always releasable while feature branches stay short.
Search across all documentation pages
Trunk-based flow keeps main always releasable while feature branches stay short.
Go teams pair that model with CI that runs go test ./..., go vet, and go mod tidy checks on every push.
Most Go libraries and services ship from a single integration branch.
Developers branch for hours or days, merge through pull requests, and tag semver releases from the same line.
Long-lived release branches are reserved for supported major versions, not everyday feature work.
That rhythm matches how module proxies pick versions from tags on the default branch.
Quick-reference recipe card - copy-paste ready.
git switch main && git pull --ff-only
git switch -c feat/short-lived-change
# edit, test locally
go test ./...
go mod tidy && git diff --exit-code go.mod go.sum
git push -u origin feat/short-lived-change
# open PR, merge when CI green, delete branchWhen to reach for this:
go.sum merge pain from stale branches.v1.x releases from main without a GitFlow release branch.A library team ships metrics API improvements:
# Day 1
git switch -c feat/metrics-api
# implement + tests
go test ./...
git push -u origin feat/metrics-api
# PR opened, CI runs on ubuntu with Go 1.26.x
# Day 2 - rebase onto fresh main
git fetch origin
git rebase origin/main
go test ./...
git push --force-with-lease
# After approval
# squash-merge or merge commit per team policy
git switch main && git pull
git tag -a v1.5.0 -m "v1.5.0"
git push origin v1.5.0What this demonstrates:
main (trunk) holds the canonical module state.go test, lint, and often go mod tidy drift detection.Trunk-based does not mean "commit directly to main without review."
It means integrate frequently and avoid parallel long-lived lines that diverge in dependencies.
| Branch | Lifetime | Purpose |
|---|---|---|
main | Permanent | Integration + release source |
feat/*, fix/* | Hours to few days | Individual changes |
release/v1 (optional) | Months to years | Patch-only backports for old major |
hotfix/* | Hours | Urgent patch, may cherry-pick to release branch |
| Check | Why it matters |
|---|---|
go test ./... | Catches broken packages before merge |
go vet ./... | Static checks for common mistakes |
go mod tidy + diff | Prevents surprise dependency drift on trunk |
golangci-lint | Team style and bug patterns |
| Race detector (select jobs) | Concurrency regressions |
Squash-merge keeps trunk history one commit per PR.
Rebase-merge preserves individual commits when they are already well formed.
Rebase feature branches onto main before merge to surface conflicts early, especially in go.sum.
go.sum and generated files conflict often. Fix: slice work into smaller PRs or merge main daily.go.mod - Dependency changes still need full test. Fix: path filters exclude only true doc paths.--force-with-lease.| Alternative | Use When | Don't Use When |
|---|---|---|
| GitFlow (develop + release) | Heavy staged QA with parallel versions | Small Go library with continuous trunk releases |
| Release branches per major | Long-term v1 patches while v2 evolves on trunk | Team lacks cherry-pick discipline |
| Stacked PRs / graphite flow | Large change split for review | CI cannot run partial graphs cheaply |
| Direct commits to main | Solo maintainer, low risk | Team repo without required checks |
Aim for one reviewable PR, usually under 400 lines of Go change.
If longer, split by package or behavior behind feature flags.
Squash keeps trunk readable and maps one PR to one changelog entry.
Preserve multi-commit history when commits are already atomic and messages are curated.
They do not, if tags on each branch follow semver and module paths stay consistent.
Document which major line each release branch maintains.
Require PR reviews, disallow force-push on main, and require status checks for test + tidy.
Optional: require signed commits or signed tags for releases.
Accept one side, run go mod tidy, re-run tests, commit the tidy result.
Never hand-edit checksum lines without understanding the dependency change.
Yes, with per-module CI paths and clear tagging docs.
go.work local dev does not replace trunk integration policy.
When release certification takes weeks and multiple versions receive patches in parallel.
Even then, keep main close to releasable to reduce merge cost.
Branch from the release commit or supported release branch, cherry-pick to trunk, tag patch semver.
Verify go test ./... on both lines when dual maintenance is active.
Flags let trunk integrate incomplete work without exposing behavior.
Go uses build tags or runtime config; branch lifetime can still stay short.
Update module README, CI triggers, and branch protection after master to main renames.
Module proxies follow tags on the current default branch.
Contributors still use forks and PRs.
Maintainers integrate to the canonical trunk quickly after review.
Tag after merge when release notes are ready.
Do not tag mid-PR; consumers need commits that passed full CI on trunk.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026