PR Rules and Branch Protection: Automating Quality Gates


🧭 Introduction: Branches Are the Front Line of Quality
By now, we’ve built pipelines that validate our code, secure our dependencies, and alert us when something goes wrong — across PRs, feature branches, post-merge flows, and nightly jobs.
But all of that can fall apart if a single thing happens:
A pull request gets merged without passing those checks.
That’s where branch protection rules and PR requirements come in.
They’re not just configuration — they’re the enforcement mechanism that connects our automated quality gates to our development process.
In previous posts, we discussed how different validations can run in different workflows and stages (push, PR, merge, nightly). But here’s the catch:
Not all status checks are treated equally by default — and not all tools enforce their outcome the way we want.
If we want to guarantee that no code gets merged unless all critical quality gates are met, we need to:
Link pipeline status checks directly into branch protection rules
Define non-negotiable conditions for PR completion
Consider custom enforcement if the platform doesn't support conditional logic across multiple pipelines
1. 🎯 What Are Branch Protection Rules (and Why They Matter)
Branch protection rules allow you to define what must happen before a branch can be updated.
Examples:
Prevent direct pushes to
main
Require PRs for any changes
Block merge unless checks pass
Enforce signed commits
Prevent force-pushes or history rewrites
These rules reduce manual errors and make pipelines enforceable, not just informational.
2. ✅ PR Rules: Guardrails for Collaboration
These are controls around how a pull request gets completed:
Require reviewers (1+, or from specific teams)
Require linear history (e.g., squash merges)
Require specific labels (e.g., "approved", "changelog updated")
Require status checks to pass (pipeline outputs)
Block merge until conversations are resolved
These rules automate good habits and prevent premature merges.
You can also create exceptions (e.g., for hotfix branches) and enforce approval from a platform team or security officer.
3. 🧪 Enforcing Quality Gates via Status Checks
All the great validations from our pipelines — tests, coverage, SAST, secrets scans — are only useful if they're enforced.
GitHub, GitLab, and Azure DevOps let you:
Require status checks to pass before merging
Define which jobs count (you must name them explicitly)
Use a single composite check (if you build a quality gate API)
You can:
Use GitHub's check-run APIs to report results manually
Let GitHub Actions auto-create checks from each job
Combine multiple workflow outcomes into a single logical pass/fail
4. 🔐 Preventing Bypass: Signed Commits, Admin Overrides, and Escalation Paths
Some organizations go further:
Require signed commits
Disallow merge via web UI
Block admins from bypassing checks
You can also create escalation mechanisms:
A “force merge” label that is only usable by admins
A manual job that promotes a branch only after an out-of-band approval
5. 🔁 Governance as Code: Managing Rules at Scale
Tools for enforcing PR rules and branch protection across many repos:
GitHub CLI + scripting
repo-settings (YAML → GitHub settings)
GitHub API
You can:
Dry-run protection changes
Create dashboards to see who has outdated rules
Auto-update repos to enforce policy
This turns protection from a one-time config into a living part of your platform.
✅ Conclusion: Quality Without Review Is Just a Hope
Branch protection and PR rules are your last safety net — the place where all your pipeline work becomes enforceable policy.
By using native rules, status checks, and custom logic, you can:
Ensure no PR gets merged unless the code is truly ready
Build trust between teams and platform automation
Keep governance invisible, automated, and scalable
Use protection rules not as a blocker — but as a contract.
If it passes, we can merge. If it doesn’t, we improve.
🧪 Example: Enforcing a Custom Quality Gate with External Status Checks
In some cases, you may want to combine outputs from multiple pipelines or external tools into a single status check that blocks pull requests until everything passes. GitHub allows this through the Checks API — giving you control over when and how a PR is approved for merge.
✅ When to Use This
You have multiple workflows or tools running (SAST, tests, coverage, SBOM)
You want one single quality gate to combine all these results
You need to wait for all builds to complete before merging
🔧 How It Works
A PR is opened or updated (via webhook or polling)
Your system (or a centralized Quality Gate API) creates a GitHub check with status
in_progress
GitHub Actions workflows run various validations
Each job calls your API to report its result
Once your API has all required outcomes, it marks the check as
completed
with asuccess
orfailure
🧠 GitHub won’t automatically trigger your Quality Gate API — you must integrate it manually via webhook, scheduler, or pipeline step.
🛠 Example Code: Reporting Results to GitHub Checks API
import requests
GITHUB_TOKEN = "your-token"
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json"
}
# Step 1: Create check as "in progress"
create_resp = requests.post(
"https://api.github.com/repos/your-org/your-repo/check-runs",
headers=headers,
json={
"name": "quality-gate",
"head_sha": "your-pr-head-sha",
"status": "in_progress",
"started_at": "2024-04-10T12:00:00Z"
}
)
check_id = create_resp.json()["id"]
# Step 2: Wait for validations to complete (or API gets results from other jobs)
all_passed = True # Your aggregation logic here
# Step 3: Mark check as completed
requests.patch(
f"https://api.github.com/repos/your-org/your-repo/check-runs/{check_id}",
headers=headers,
json={
"status": "completed",
"conclusion": "success" if all_passed else "failure",
"completed_at": "2024-04-10T12:10:00Z",
"output": {
"title": "Quality Gate",
"summary": "All required checks passed" if all_passed else "One or more checks failed",
"text": "Tests: OK, Lint: OK, SAST: OK, SBOM: OK"
}
}
)
⚙️ How to Trigger the API
You have three options:
✅ From GitHub Actions: each workflow step calls your Quality Gate API with its result
✅ Via GitHub webhook: listen to
workflow_run.completed
and poll results✅ Via scheduler: your API polls GitHub for statuses and decides when to conclude
🔐 Enforcing in Branch Protection
In GitHub:
Go to Settings → Branches → Protection Rules
Require status checks to pass
Add
quality-gate
to the list
Now, PRs won’t merge unless this external status passes.
🧷 What If the API Fails or Times Out?
If your service never marks the check as completed
, the PR stays blocked indefinitely.
Best practices:
Monitor stale checks
Set timeouts (e.g., auto-fail after 30 min)
Send Slack/email/GitHub alerts if validation is stuck
Retry failed API calls automatically
✅ Summary
External status checks give you:
Full control over complex CI/CD validations
The ability to combine jobs, tools, and policies
Enforceable quality gates — even across teams or repos
This is the final mile of PR automation: smart, scalable, and secure.
Subscribe to my newsletter
Read articles from Claudio Romão directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
