Git & GitHub Best Practices
Portable Git and GitHub habits for Go module repos.
These rules keep trunk releasable, module graphs tidy, and releases trustworthy for downstream go get.
How to Use This List
- Apply when bootstrapping a repo, updating branch protection, or preparing a release.
- Automate enforceable items in GitHub Actions (test, vet, tidy, lint).
- Revisit when adding a second module, going private, or supporting multiple major lines.
- Treat unchecked items as release or security debt, not style preferences.
A - Trunk and branching
- Keep
mainalways releasable. Every merge should passgo test ./...and module tidy checks in CI. - Use short-lived feature branches. Integrate within days to limit
go.summerge pain. - Protect the default branch. Require PR review, disallow force-push, and require status checks.
- Delete merged branches. Reduces clutter and mistaken pushes to stale lines.
- Document release branch policy. State when
release/vNexists and how patches cherry-pick from trunk.
B - Commits and history
- Write imperative commit subjects with package scope. Example:
internal/parse: reject empty input. - Commit
go.modandgo.sumwith the code that caused dependency changes. Never split across unnoticed merges. - Avoid rewriting public history after tags consumers use. Prefer
retractand forward-fix tags. - Use
--force-with-leaseonly on personal feature branches. Never on shared or release branches. - Separate mechanical refactors from behavior changes. Easier review and safer bisect.
C - Pull requests and review
- Keep PRs reviewable (often under 400 lines of Go change). Split large features by package or flag.
- Fill a template: summary, module impact, test plan. Reviewers prioritize API and
go.moddiffs. - Require green CI before merge. Include tidy drift check, not only tests.
- Use draft PRs until tests and description are complete. Reduces noise for reviewers.
- Label breaking changes and dependency bumps. Feeds changelog and release notes.
D - Go module releases
- Tag semver releases from vetted trunk commits. Pattern
v1.2.3matchinggo.modmajor line. - Use annotated tags for releases; sign tags when policy requires.
git tag -aorgit tag -s. - Push tags explicitly.
git push origin v1.2.3- tags are not default on plain push. - Use
/v2module paths for incompatible API changes. Do not break consumers under the v1 import path. - Retract bad releases in
go.modinstead of deleting tags. Document migration in changelog.
E - CI, secrets, and private modules
- Pin CI Go toolchain to
go.modgodirective. Usego-version-filein setup-go. - Cache modules via
go.sumkeys. Include nested sums in monorepos. - Set
GOPRIVATEfor org modules and scope narrowly. Avoid disabling sumdb for public deps. - Store PATs and publish tokens in GitHub secrets with least privilege. Document local dev auth parity.
- Run
GOWORK=offtests before tagging monorepo modules. Simulates consumer resolution.
F - Repo hygiene
- Maintain a Go-aware
.gitignore. Binaries, profiles, IDE noise; never ignorego.sum. - Use
.gitattributesmerge strategy forgo.sumif needed.unionreduces manual conflict lines. - Do not commit machine-specific
replacepaths. Usego.worklocally or team-committed workspace files. - Keep LICENSE and README module path accurate. Consumers and proxies expect consistency with
go.mod. - Record version in built binaries when operability matters.
-ldflagsfromgit describeor CI env.
FAQs
What is the single highest-impact rule?
Never merge to trunk without CI that runs go test ./... and fails on tidy drift.
Everything else supports that gate.
Should we squash all PRs?
Squash keeps trunk readable for libraries.
Choose rebase-merge if commit-level history is curated and reviewers want granular archaeology.
How often should we tag releases?
Libraries: when API or fixes warrant semver.
Services: tag deployable commits; consumers may be internal only.
Are signed commits required?
Signed commits help provenance.
Signed release tags are often higher ROI for module consumers than signing every commit.
What belongs in CODEOWNERS for Go monorepos?
Per-module paths for go.mod and exported packages.
Require owner review when public API or dependencies change.
Should dependabot bump Go modules automatically?
Useful with CI and human review on transitive changes.
Blind merges risk supply-chain surprises.
How handle fork contributions?
Maintainers cherry-pick or merge from fork PRs after CI.
Do not grant secrets to untrusted fork workflows.
When is vendoring appropriate?
Air-gapped CI, regulated environments, or pinned disaster recovery.
Document vendor refresh cadence in README.
What merge queue settings help Go repos?
Enable when many PRs merge hourly and trunk tests are reliable.
Requires consistent CI duration under team SLA.
How align Git tags with container image tags?
Use the same semver string where possible.
Build images from tagged SHAs in CI, not floating branch heads.
Should we commit go.work?
When the whole team shares the same workspace modules.
Personal supersets stay untracked.
What is a common first release mistake?
Tagging before /v2 path migration on a breaking change.
Consumers pin broken import graphs.
Related
- Git for Go Teams - Conceptual overview of Git plus modules
- Branching Strategies & Trunk-Based Flow - Trunk workflow detail
- GitHub Actions for Go Repos - Automating enforceable checks
- Signed Tags & Go Module Releases - Release tagging depth
- Code Review with GitHub PRs - PR templates and review focus
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).