PR Rules and Branch Protection: Automating Quality Gates

Claudio RomãoClaudio Romão
6 min read

🧭 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

  1. A PR is opened or updated (via webhook or polling)

  2. Your system (or a centralized Quality Gate API) creates a GitHub check with status in_progress

  3. GitHub Actions workflows run various validations

  4. Each job calls your API to report its result

  5. Once your API has all required outcomes, it marks the check as completed with a success or failure

🧠 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.

0
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

Claudio Romão
Claudio Romão