bbabafemi
All posts
DevOps

Branch policies in Azure Repos: a production-ready setup

The branch protection settings I configure on every Azure Repos repo to keep main always shippable — and the ones I deliberately don't enable.

October 21, 2025 3 min readby Babafemi Bulugbe

Azure Repos branch policies are the difference between a main branch you trust and a main branch you cross your fingers about. Here's the configuration I apply on every production repo.

The mandatory four

These get enabled on main from day one. Anything less is reckless.

1. Require a minimum number of reviewers

Setting: Require 1–2 reviewers. Reset votes when new changes are pushed. Allow requestors to approve their own changes: off.

For a small team (3–5 engineers) one reviewer is fine. For a larger team, two. Allowing self-approval defeats the entire point — turn it off.

2. Check for linked work items

Setting: Required.

This forces every PR to be linked to a work item. Three benefits:

  • Traceability — you can answer "why does this code exist?" months later.
  • Discipline — engineers think about why before they merge.
  • Reporting — your sprint metrics are actually meaningful.

3. Check for comment resolution

Setting: Required.

If a reviewer left a comment, it has to be marked resolved before merge. This stops the bad pattern of "I'll address that later" merges.

4. Build validation

Setting: Add the CI build pipeline. Required.

Branch policy that requires the build to pass on the PR's source branch (with the target merged in). This is the actual protection in branch protection — code that doesn't compile or that breaks tests literally cannot reach main.

5. Status checks from external services

If you have SonarQube, CodeQL, or any other SAST/quality tool, hook it in as a status check.

Setting: Required if you have it; otherwise add it as soon as you set the tool up.

6. Limit merge types

Setting: Allow squash merge only.

Controversial — some teams love rebase, some love merge commits. My take: squash merge as the default rule because it gives you a clean linear history on main, and a 1:1 mapping between PRs and commits. Easy to revert, easy to bisect.

If you have specific repos where you want full history (e.g. a long-running infrastructure repo), override the policy on a per-repo basis.

The settings I don't enable

Required reviewers from specific groups

I avoid this on most repos because it creates merge bottlenecks. The exception is:

  • The infrastructure repo — require platform team reviewer.
  • The security repo — require security team reviewer.

For everything else, the engineering team reviews each other and that's the right answer.

Require a clean build before merge for every push

Tempting, but the build queue gets long. Use the "build validation" policy with appropriate filters and validDuration set to a few hours, not zero.

Set it up via the API, not the UI

The portal is fine for one repo. Once you have ten, you want this in code:

# Get the repo and ref
REPO_ID=$(az repos show --org $ORG --project $PROJECT --repository my-repo --query id -o tsv)
REF="refs/heads/main"

# Apply minimum reviewers policy
az repos policy required-reviewer create \
  --repository-id $REPO_ID \
  --branch main \
  --required-reviewer-ids $TEAM_GROUP_ID \
  --message "Approval required" \
  --org $ORG --project $PROJECT

Better still, manage policies via Bicep / Terraform. The azuredevops_branch_policy_* Terraform resources cover the full surface.

The principle behind all of this

A branch policy isn't to slow developers down — it's to make main always in a state you'd be willing to ship. If something on main is broken, your release process slows; if your release process slows, hotfixes get scary; if hotfixes get scary, your team starts working around the system.

Strict branch policies are how you avoid all of that. Make main boring. Boring is the goal.