Repository and branching
A branching model is a contract about how change reaches production. This one is deliberately simple: short-lived feature branches, three long-lived environment branches, promotion by pull request, and no direct pushes.
Long-lived branches
| Branch | Environment | Purpose | Protection |
|---|---|---|---|
main | Production | Stable, released code | Two reviews, all checks, no direct push, no force push |
stage | Staging | Integration and user acceptance testing | One review, all checks |
dev | Development | Ongoing integration of completed work | One review, build and unit tests |
A team that deploys continuously may collapse stage into an ephemeral
environment. The rule that does not bend: production is reached only through a
reviewed pull request from a branch that has passed its gates.
Short-lived branches
Created from dev, merged back within days rather than weeks.
feature/<short-description>
bugfix/<short-description>
hotfix/<short-description>
chore/<short-description>
TICKET-123-feature-name
Long-lived feature branches recreate the merge conflicts that trunk-based development avoids. If a branch lives longer than a sprint, split it.
Commit messages
TICKET-123 Add retry with backoff to payment capture
The gateway returns 503 under load without retry, causing
avoidable failures on checkout. Adds three attempts with
exponential backoff and a circuit breaker after five
consecutive failures.
Rules:
- Start with the ticket identifier.
- Subject in imperative mood, under 72 characters.
- Body explains why, not what — the diff shows what.
- Reference breaking changes explicitly.
Pull request workflow
Create the branch From
dev, with a name that states the change.Open a draft early A draft PR makes the change visible while it is still cheap to redirect.
Pass the gates Build, unit tests, static analysis, secret scan, and infrastructure plan where applicable.
Review At least one reviewer; two for infrastructure, security-sensitive paths, or anything that changes a data store.
Merge to dev Squash merge for a single coherent commit. Linear history.
Promote PR from
devtostageafter development validation; PR fromstagetomainafter UAT.Deploy The pipeline deploys the merged branch. No manual deployment step.
Delete the branch The branch has served its purpose; keeping it invites divergence.
What reviewers check
Code review is not a style exercise — linters handle style. Reviewers look for:
- Correctness under failure. What happens when the dependency times out, the queue backs up, or the request is retried?
- Blast radius. What breaks if this is wrong, and how quickly would we know?
- Test quality. Do the tests exercise behaviour or implementation details?
- Security. Does this widen a permission, log a secret, or trust input?
- Operability. Can this be diagnosed in production from logs and metrics?
- Reversibility. Can this change be rolled back without data loss?
Branch protection
| Rule | main | stage | dev |
|---|---|---|---|
| Require pull request | Yes | Yes | Yes |
| Required approvals | 2 | 1 | 1 |
| Dismiss stale approvals on new commits | Yes | Yes | Yes |
| Require status checks | Yes | Yes | Yes |
| Require branch up to date | Yes | Yes | No |
| Require conversation resolution | Yes | Yes | No |
| Restrict who can push | Yes | Yes | — |
| Allow force push | No | No | No |
| Allow deletion | No | No | No |
| Require signed commits | Per customer | Per customer | Per customer |
Hotfixes
A production defect that cannot wait for the normal promotion path:
- Branch
hotfix/<description>frommain. - Fix, test, and open a PR to
mainwith an incident reference. - Review and deploy through the same pipeline — faster, not bypassed.
- Merge
mainback intostageanddevso the fix is not lost on the next release.
The hotfix does not skip the pipeline. It skips the queue.
Anti-patterns
- Direct commits to
main"only this once." - Long-lived release branches that diverge from
main. - Force-pushing a shared branch and rewriting others' history.
- Merging without the required checks because they were slow.
- Feature flags used to hide a half-finished merge on
mainindefinitely.