Git, CI/CD & GitOpsXXXII · Protected BranchesFoundations
What branch protection is — server-side controls on a branch
What you'll learn
- Define branch protection as a server-side rules engine attached to a named branch
- Distinguish controls the forge enforces from controls a client may bypass
- Identify what the server refuses versus what the server merely discourages
- Recognise branch protection as a layer in the governance stack, not a stand-alone control
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Branch protection is a server-side rules engine attached to a named branch. The rules are configured by repository administrators and evaluated by the forge on every push, fetch, and merge attempt against that branch. A client - a developer laptop, a CI runner, a compromised credential, a force-push - cannot bypass the rules by running a different command or using a different protocol. The rules are not opinions; they are server rejections.
This is the boundary that makes branch protection a control at all.
A pre-receive hook on the client, a pre-commit script in the
repository, or a team norm that “we always do X” is client-side
trust. It works until it does not - until the client does not run
it, until the hook is skipped, until the norm is forgotten. Server-
side rules work because they are evaluated by the system that
already received the change. A push that does not satisfy the rule
is a push that does not land.
What branch protection enforces
A branch protection rule is a set of conditions that the forge checks at receive time. The conditions vary by forge, but the canonical set is:
flowchart LR
A[Push or merge attempt] --> B{Forge evaluates rule}
B --> C{Branch matches?}
C -->|no| D[Default behaviour applies]
C -->|yes| E{Rule checks pass?}
E -->|no| F[Server rejects]
E -->|yes| G[Server accepts]
- Branch matching. A protection rule applies to one or more
branch patterns:
main,release/*,production. The forge matches the target ref against the pattern set and applies the rules that match. - Direct push restrictions. Force pushes and direct commits are refused; the branch must accept changes only via merging a pull request or via a privileged push path (covered in lesson 2).
- Required approvals and reviewers. A configurable minimum number of approving reviews, with optional CODEOWNERS review (covered in lesson 3).
- Required status checks. A set of CI checks that must report success before the merge is accepted.
- Bypass configuration. A named list of actors who can override the rules under logged conditions (covered in lesson 4).
- Tag and ref-side rules. A separate but parallel rules engine for tags, covered in lesson 5.
The forge is the enforcement point. The repository is not the enforcement point. The client is not the enforcement point. A rule that the forge does not evaluate is, by definition, a rule the client can ignore.
What clients cannot bypass
A useful exercise is to enumerate the things a client cannot do once a rule is enabled on the server:
- A client cannot land a direct commit to a protected branch if the rule says “no direct pushes”.
- A client cannot force-push over a protected branch if the rule disables force pushes (and, on GitHub, if the rule exists at all - force push is refused by default once protection is on).
- A client cannot merge a pull request that has fewer approvals than the configured minimum.
- A client cannot merge a pull request whose branch is behind the base, if the rule requires a “merge commits must be up to date” status check that has not yet re-run against the new head.
- A client cannot merge a pull request that violates a required status check, even if the check was added after the PR was opened (the rule can require a fresh run on the latest commit).
These five refusals are the core. They are refusals because the forge is the only system that can land a commit on the server, and the forge is the only system that has the rules attached.
The boundary between forge and client
Branch protection stops at the forge’s understanding of a branch. There is a wider set of operations the forge does not know about, and a wider set the forge will never see:
- The forge does not know about your local commits. The forge sees only what you push. A history rewrite that is never pushed is invisible to the forge and to the audit log; you can rewrite your local history freely. Once you push - or push the rewrite - the forge sees it.
- The forge does not know about your CI credentials. A CI pipeline that merges a pull request is a CI pipeline with merge rights, not a CI pipeline with a status-check role. The two are configured separately in forge permissions, and conflating them is a common control failure.
- The forge does not know what your local working tree looks like. A bad commit that never gets pushed is a commit that, from the forge’s point of view, does not exist.
Production discipline
The production framing of branch protection has four rules that recur throughout this part:
- One source of truth for “what is protected”. Branch protection is configured in the forge UI or API. The same configuration cannot be in a README, in a wiki, or in a team norm - if the UI says “main is protected”, main is protected; if the UI does not, nothing else makes it so.
- Default branches are protected by default in any serious
repository. If your
mainbranch is not protected, your repository is not in scope for this part of the course. The default behaviour of any new rule must be to add the default branch. - Protection is configured once and audited continuously. Rules drift. New administrators widen the bypass list. UI refactors change defaults. The branch protection configuration must be version-controlled, reviewable, and reviewed like any other production rule.
- Protection is never sufficient alone. Branch protection blocks bad pushes. It does not block a bad dependency, a bad signed commit by a compromised key, or a bad actor with admin rights. The stack is the control; protection is one rule in it.
Cross-course references
- Linux for Production Sysadmins - Part XIV (AuditLogging) covers audit logging; the role of audit logs is the downstream detection layer for protection bypass.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers branch protection as part of the repository architecture pattern, alongside signing and tag discipline.
- Terraform for Production Sysadmins - Part XXXII (RepoGuard) covers the same boundary in Terraform-specific terms: what the server enforces versus what the client may bypass.
Quiz
Knowledge check · 4 questions
Q1. An engineer runs a pre-commit script locally that blocks a forbidden file pattern, but a second engineer bypasses the script and pushes directly. What does branch protection add that the script did not?
Q2. Once a branch is protected, a client can still rewrite its local history freely without the forge knowing.
Q3. List three refusals that a properly configured branch protection rule can issue to a client attempting a push or merge.
Q4. Diagnose why a CI policy job did not catch a forbidden push that branch protection was supposed to block.
A Terraform module is pushed directly to the default branch at 02:14 UTC. The push lands. A CI policy job runs after the push and emits a high-severity alert: 'production-relevant change without an approved PR'. The team is surprised: branch protection is configured on the default branch and they believed direct pushes were blocked.
Passing score: 75%. Answers are checked in this browser.