A reproducible environment and a working pipeline do not, by themselves, produce a reliable delivery process. Process is the layer that decides whether the environment and the pipeline are actually used—every time, under deadline pressure, by everyone on the team, not just by the person who set them up. A workflow that only works when nobody is in a hurry is not a workflow. It is a suggestion.

The goal of this article is narrower than it sounds: not "best practices for Git," but a small, specific set of rules a three-to-eight person team can adopt without a change-management project, and that will still be followed in six months.

Why process design is architecture too

Treat the branching model, review rules, and repository conventions as decisions with the same weight as a database schema choice. Both are hard to change once a team has built habits around them. A workflow that requires two approvals on a two-person team will be bypassed constantly, which trains everyone to treat protection rules as theater. A workflow with no required checks will be fine until the day it is not, and by then the bad merge is already in main.

The right workflow is the smallest one the team will not route around.

Branch protection as the enforcement mechanism

Conventions that live only in a README are advice. Branch protection rules are the mechanism that makes the important ones non-optional. For most small teams, the practical baseline on the default branch is:

  • Require a pull request before merging—no direct pushes to main.
  • Require the CI checks (lint, typecheck, test, build) to pass before merging.
  • Require at least one approving review, with an exception path for solo maintainers documented rather than silently ignored.
  • Require branches to be up to date with main before merging, so CI is validating the code that will actually land.
  • Restrict force-pushes and deletions on the protected branch.
gh api \
  --method PUT \
  -H "Accept: application/vnd.github+json" \
  repos/OWNER/REPO/branches/main/protection \
  -f required_status_checks.strict=true \
  -f 'required_status_checks.contexts[]=lint' \
  -f 'required_status_checks.contexts[]=typecheck' \
  -f 'required_status_checks.contexts[]=test' \
  -f 'required_status_checks.contexts[]=build' \
  -f enforce_admins=true \
  -f required_pull_request_reviews.required_approving_review_count=1 \
  -f restrictions=null

The enforce_admins=true flag matters more than it looks. A rule that administrators can bypass is a rule that gets bypassed exactly when it matters most—during an incident, under a deadline, by the person most likely to be tired and least likely to want a second reviewer.

Choosing a branching model that matches team size

Most small teams do not need GitFlow's long-lived develop, release, and hotfix branches. That model was designed for release trains with scheduled cuts and multiple concurrent supported versions. A product shipping continuously to a single production environment is usually better served by trunk-based development: short-lived feature branches, frequent merges to main, and deployment triggered from main itself (or from a main-derived tag, once the series reaches staging and production in later parts).

ModelFits whenCost for a small team
GitFlowMultiple supported release versions, scheduled release trainsExtra branches to keep in sync, merge conflicts accumulate, slower feedback
Trunk-based, short branchesContinuous delivery to one or two environmentsRequires strong CI and feature flags for incomplete work
Long-lived environment branches (staging, main)Distinct staging and production promotion stepsManageable if promotion is a deliberate, scripted merge, not an ad hoc habit

This series uses the third pattern in later parts—main deploys to staging automatically, and production deployment is a deliberate promotion—but the underlying principle holds regardless of the specific model: pick the smallest number of long-lived branches that still maps cleanly onto the number of environments the team actually operates.

Pull requests as the unit of review

A pull request should represent one reviewable idea. That sounds obvious and is routinely violated the moment a deadline appears and three unrelated fixes get bundled into a single branch because splitting them "wastes time." It does not save time; it moves the cost from the author, who understands the change, to the reviewer, who does not.

A pull request template keeps the request's shape consistent without adding real overhead:

<!-- .github/pull_request_template.md -->
## What changed
<!-- One or two sentences. -->

## Why
<!-- The problem this solves, or the issue it closes. -->

## How to verify
<!-- Steps a reviewer can run locally or check in the preview environment. -->

## Risk
<!-- Anything that touches migrations, auth, billing, or production config. -->

The "Risk" field earns its place. It gives the author a moment to flag a schema migration or a change to authentication before the reviewer discovers it by accident, and it gives the reviewer a reason to slow down on exactly the changes that deserve it.

Commit message conventions that pay for themselves

Conventional Commits (feat:, fix:, chore:, docs:, refactor:) are worth adopting for one concrete reason beyond tidiness: they make automated changelog generation and semantic versioning possible later, without retrofitting history. A team does not need to enforce this with a commit-linting bot on day one—but agreeing on the prefixes early avoids a painful cleanup later, when someone wants to generate a changelog and finds six months of wip and fix stuff commits.

Repository layout that keeps reviews legible

A pull request is easier to review when the reviewer already knows where things live. A predictable top-level layout—app/, prisma/, .github/workflows/, docs/—means a diff's location already communicates part of its intent before the reviewer reads a line of code. Resist the temptation to reorganize directories as part of an unrelated feature change; a restructuring pull request should stand alone so its diff can actually be reviewed, rather than hiding inside three hundred lines of unrelated changes.

What branch protection cannot do

  • It cannot substitute for a reviewer who actually reads the diff. A rubber-stamp approval satisfies the rule and defeats its purpose.
  • It cannot catch a mistake that CI does not check for. Protection rules are only as strong as the checks required in part five of this series.
  • It cannot resolve disagreements about scope or design. That is a conversation, not a setting.
  • It cannot make a two-person team behave like a twenty-person team. Adjust the required review count honestly rather than adopting a rule the team cannot sustain.

Common mistakes

  • Requiring two approvals on a team too small to reliably provide a second reviewer, which quietly trains everyone to bypass the rule.
  • Leaving enforce_admins off, so the rule is optional exactly when someone is under the most pressure to skip it.
  • Letting main drift out of sync with what is actually deployed, so "merged" no longer means "released."
  • Treating the pull request template as a formality and leaving every field blank.
  • Reorganizing the repository inside a feature branch, burying a structural change inside unrelated diffs.

Practical checklist

  • main is protected: no direct pushes, required status checks, required review.
  • enforce_admins is enabled so the rule applies to everyone.
  • A branching model is chosen deliberately and matches the number of environments the team operates.
  • A pull request template captures what changed, why, how to verify, and risk.
  • Commit prefixes are agreed on, even if not yet enforced by tooling.
  • The top-level repository layout is stable and predictable across pull requests.
  • Merged branches are deleted automatically to keep the branch list meaningful.

Previous: Dev Containers: Put the Developer Setup in the Repository

Next: Your First GitHub Actions Pipeline: Lint, Typecheck, Test, Build. With the process rules in place, the series builds the automated checks that branch protection actually enforces.