Protected Branch Rules
Branch protection rules enforce code review and CI requirements before changes can land on important branches. They are configured per-repository via the API and evaluated at merge time.
Rule Fields
| Field | Type | Description |
|---|---|---|
pattern |
string | Glob matched against branch names, e.g. main, release/* |
require_reviews |
int | Minimum approving reviews before merge is permitted (0 = none) |
dismiss_stale_reviews |
bool | Dismiss existing approvals when new commits are pushed |
require_status_checks |
string[] | CI context names that must pass before merge, e.g. ["ci/test"] |
restrict_pushes |
bool | When true, only repository admins may push directly |
API Reference
List Rules
GET /api/v1/repos/{owner}/{repo}/branches/protection
Authorization: Bearer <token>
Response (200)
[
{
"id": "...",
"repo_id": "...",
"pattern": "main",
"require_reviews": 2,
"dismiss_stale_reviews": true,
"require_status_checks": ["ci/test", "ci/lint"],
"restrict_pushes": true,
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
}
]
Create or Update a Rule
PUT /api/v1/repos/{owner}/{repo}/branches/protection/{pattern}
Authorization: Bearer <token>
Content-Type: application/json
The {pattern} path segment identifies the rule. If a rule for that pattern
already exists it is updated; otherwise a new rule is created.
Request body
{
"require_reviews": 2,
"dismiss_stale_reviews": true,
"require_status_checks": ["ci/test"],
"restrict_pushes": false
}
Response (200)
{
"id": "...",
"repo_id": "...",
"pattern": "release/*",
"require_reviews": 2,
...
}
Only the repository owner or a server admin may create or modify rules.
Delete a Rule
DELETE /api/v1/repos/{owner}/{repo}/branches/protection/{pattern}
Authorization: Bearer <token>
Returns 204 No Content on success.
Pattern Syntax
Patterns use Go filepath.Match glob
semantics:
| Pattern | Matches | Does not match |
|---|---|---|
main |
main |
main-old, develop |
release/* |
release/1.0, release/2.0.0 |
release/1.0/patch |
feature-* |
feature-login, feature-signup |
bugfix-login |
v[0-9]* |
v1, v2.0, v10 |
va.0, alpha |
Note:
*does not match the/separator. Userelease/*notrelease/**for a single path segment.
How Rules Are Enforced
Rules are checked at merge time via the API (POST .../pulls/{n}/merge).
The merge is rejected if:
- The target branch matches the rule pattern, and
- The number of approving reviews is below
require_reviews, or - Any context listed in
require_status_checkshas not passed.
Direct Push Restriction
When restrict_pushes is true, the git pre-receive hook (Feature 01) checks
whether the pushing user is a repository admin. Non-admin direct pushes to
matching branches are rejected at the protocol level.
Example Configurations
Protect main — require two reviews and CI
PUT /api/v1/repos/alice/myrepo/branches/protection/main
{
"require_reviews": 2,
"dismiss_stale_reviews": true,
"require_status_checks": ["ci/test", "ci/coverage"],
"restrict_pushes": true
}
Protect all release branches — require one review
PUT /api/v1/repos/alice/myrepo/branches/protection/release%2F*
{
"require_reviews": 1,
"dismiss_stale_reviews": false,
"require_status_checks": ["ci/test"],
"restrict_pushes": false
}
URL-encode
/as%2Fin the path segment when the pattern contains slashes.
Relationship to Feature 01 protected_branches
Feature 01 introduced a basic protected_branches table used exclusively by the
git pre-receive hook to determine whether a direct push is allowed. Feature 02
adds the richer protected_branch_rules table exposed via this API, which adds
review-count gates and status-check requirements evaluated at merge time.
The two tables are independent; you can configure both or only one depending on how strictly you want to enforce policy.
Required-pipeline gate (require_pipeline)
The required-pipeline gate makes a branch refuse changes until the relevant
commit has a passing CI pipeline. It is configured on the simple
protected-branch model (the protected_branches table the git push / merge /
MR-open paths read), via its own API:
GET /api/v1/repos/{owner}/{repo}/protected-branches
PUT /api/v1/repos/{owner}/{repo}/protected-branches/{pattern}
This is a different surface from the
/branches/protection/*Feature-02 API documented above. The Feature-02require_status_checksfield is separate and is not what drives this gate.
Rule fields (simple model)
| Field | Type | Default | Description |
|---|---|---|---|
pattern |
string | — | Glob matched against branch names (path segment of the PUT URL) |
restrict_push |
bool | false |
When true, only repo admins / push-allowlist members may push directly |
required_reviews |
int | 0 |
Minimum approving reviews before merge |
require_pipeline |
bool | false |
When true, enable the required-pipeline gate on this branch |
Enabling the gate
PUT the rule with require_pipeline: true (repo-admin permission required):
PUT /api/v1/repos/alice/myrepo/protected-branches/main
{
"restrict_push": false,
"required_reviews": 0,
"require_pipeline": true
}
The {pattern} path segment may contain slashes directly (e.g.
protected-branches/release/*) or percent-encoded (release%2F*).
Verdict: soft vs. hard
The gate classifies the latest terminal pipeline for the commit it is checking into one of five verdicts:
| Verdict | Meaning |
|---|---|
success |
No failed or cancelled jobs. |
soft_only |
Every failure is a tolerated allow_failure job; no hard failures. Treated as a pass. |
hard_failure |
At least one non-tolerated failed job, or any cancelled job. |
pending |
A pipeline exists but the latest one has not reached a terminal state. |
missing |
No pipeline exists for the commit at all. |
The verdict is recomputed live from current job state on every check, so retrying a pipeline immediately reflects the new outcome.
What the gate does on each surface
The gate is enforced on three surfaces, each with deliberately different strictness:
| Surface | Allowed verdicts | Blocked verdicts | On block |
|---|---|---|---|
| Merge | success, soft_only |
hard_failure, pending, missing |
409 Conflict with a machine-readable reason (pipeline_hard_failure / pipeline_pending / pipeline_missing) |
| MR-open | success, soft_only, pending, missing |
hard_failure only |
409 Conflict (pipeline_hard_failure) |
| Push | every verdict except a hard_failure tip |
hard_failure (on the pushed tip) |
push rejected at the git protocol with a remote message |
Notes:
- Merge is the strict surface. A merge into a gated branch is only allowed
once CI has actually finished and passed (or soft-only-failed).
pendingandmissingboth block the merge. - MR-open is the lenient surface. You can open a merge request toward a
gated branch while CI is still running (or before any pipeline exists) —
pendingandmissingare allowed. Only a confirmedhard_failureblocks the open. - Push fails open. The push gate only rejects when the pushed tip already
has a
hard_failureverdict — which is rare in practice, because a push is usually what triggers the pipeline in the first place. A push that has no pipeline yet (missing) or one still running (pending) is allowed through so CI can start. Repository admins and push-allowlist members bypass the push gate entirely.
Independent of restrict_push
require_pipeline and restrict_push are independent toggles. A rule with
require_pipeline: true, restrict_push: false gates merges / MR-opens on CI and
rejects a push whose tip is a hard failure, but does not otherwise restrict
direct pushes — a normal non-admin push that advances the branch (and triggers a
new pipeline) is still allowed. Set restrict_push: true separately if you also
want to forbid non-admin direct pushes.