---
title: Repo governance and branch protection
slug: repo-governance-and-branch-protection
summary: The minimum controls every repository needs (protected default branch, required review, CODEOWNERS, no force-push, clean group structure) and how to audit them.
order: 11
updated: 2026-11-11
status: ready
---

Use this page when setting up a new repo or group, after a migration, or for a periodic audit. In a regulated environment these controls are often audit evidence, so they must be on everywhere and provable.

## The baseline for every repo

- [ ] Default branch is protected: no direct push, no force-push, no deletion.
- [ ] Merges need at least one approval from someone other than the author; two for infrastructure and production config.
- [ ] Approvals reset when new commits are pushed.
- [ ] `CODEOWNERS` routes reviews for sensitive paths (CI files, IaC, IAM).
- [ ] Pipeline must pass before merge.
- [ ] Secret scanning and dependency scanning run on every MR.
- [ ] Release tags are protected.
- [ ] Repo has an owner team, a description and a README that says what it deploys and where.

## Organise groups by ownership, not by history

```text
<org>/
  platform/        shared CI templates, IaC modules, tooling
  infrastructure/  live Terraform per account or environment
  services/<team>/ application repos, one subgroup per owning team
  archive/         read-only, kept for history
```

- [ ] Permissions are granted to groups (synced from the identity provider), never to individuals.
- [ ] Maintainer and Owner roles are rare and reviewed quarterly.

## CODEOWNERS example

```text
# Everything needs a team reviewer
*                    @<org>/<team>
# CI and deploy config need platform review too
ci-config.yml        @<org>/platform
/ci/                 @<org>/platform
# IaC and IAM need two groups
/terraform/          @<org>/platform @<org>/security
```

## Audit with the API (read-only)

```bash
# Protected branches for one project (swap the CLI for your git host)
git-host api "projects/<url-encoded-path>/protected_branches" | jq '.[] | {name, allow_force_push, code_owner_approval_required}'

# Approval rules
git-host api "projects/<url-encoded-path>/approval_rules" | jq '.[] | {name, approvals_required}'

# Every project in a group, with default branch and visibility
git-host api --paginate "groups/<group-id>/projects?include_subgroups=true&per_page=100" \
  | jq -r '.[] | [.path_with_namespace, .default_branch, .visibility, .archived] | @tsv'
```

Loop the first two over the third to get a full report. Output a table of repos that miss any baseline item and open one MR or ticket per fix.

## Apply settings as code

Use your git host's Terraform provider (or the platform's equivalent) for protected branches and approval rules, so changes are reviewed and drift is visible. See [Terraform changes via merge request](./terraform-changes-via-merge-request.md).

```hcl
resource "git_branch_protection" "default" {
  project            = "<project-id>"
  branch             = "main"
  push_access_level  = "no one"
  merge_access_level = "maintainer"
  allow_force_push   = false
  code_owner_approval_required = true
}
```

## Pitfalls

- Admins exempt from protection "for emergencies". Emergencies are when mistakes happen. Use a break-glass process with an audit trail instead.
- Personal access tokens with `api` scope used by CI. Use project or group tokens with the smallest scope and an expiry, or OIDC.
- Archiving a repo that something still clones at build time. Search CI configs for the URL first.
