GitHub Branch Protection Rationale

Why the recommended repository protections exist, the risks they mitigate, and how they interact with continuous integration in a monorepo.

To apply these settings, follow Configure GitHub Repository Protection.

Table of Contents

  1. Force-push and deletion risks
  2. Why required status checks matter
  3. Required checks in a monorepo
  4. Why require a pull request before merging
  5. Status checks or pull requests: why it is not a choice
  6. Keeping main green with direct pushes

Force-push and deletion risks

The default branch (main) is the source of truth for the project. Without protection, any collaborator with write access can rewrite its history with a force push or delete it entirely — actions that are very difficult to recover from and can corrupt the shared history for every contributor.

Risk Consequence
Force push Rewrites the commit history of main. Other contributors’ local clones now diverge from the remote, causing confusing conflicts or silent data loss. CI runs attached to the overwritten commits disappear from the audit trail.
Branch deletion The main branch reference is removed from the remote. Any open pull request targeting main is immediately closed, and the default branch of the repository becomes undefined until it is recreated.

Enabling these two protections is a minimal, zero-friction safeguard: it does not require status checks to pass, does not block any normal merge workflow, and takes effect instantly.

If GitHub shows the advisory message “Protect this branch from force pushing or deletion, or require status checks before merging” on your branch protection rule, enabling the two settings is the lightweight way to satisfy that warning without turning on required status checks.

Why required status checks matter

A status check is a signal that a CI job reports back to GitHub after running on a pull request. GitHub can be configured to block merges until specific status checks have passed. This turns the CI pipeline from an informational indicator into an enforced quality gate: no code reaches main unless it has been built and tested.

Without required status checks, a pull request can be merged even when CI is red — or when CI never ran at all. This allows broken code to reach main, defeats the purpose of the CI pipeline, and often creates extra work restoring a stable state.

Required checks in a monorepo

In a monorepo, a second problem appears. GitHub’s native paths filter on the pull_request trigger silently skips the entire workflow when none of the listed paths are changed. A skipped workflow reports no status to GitHub, so GitHub treats the check as absent rather than as passed. If that check is listed as required, the pull request is permanently blocked — even for changes that genuinely do not need that job to run (for example, a documentation-only change that does not touch backend/**).

The fix is to keep the pull_request trigger unconditional and move the path logic inside the workflow, so every pull request runs the workflow and the workflow itself decides whether to build or to report a lightweight passing skip-check. Either way GitHub always receives a green status for the required check, so the branch protection rule is satisfied without blocking unrelated pull requests. The concrete workflow shape is documented in GitHub CI Path Filtering.

Why require a pull request before merging

Required status checks only apply to pull request merges. A collaborator with write access can still push commits directly to main, bypassing CI entirely. Requiring a pull request before merging closes this gap: direct pushes are rejected, so every change must arrive via a PR — and therefore must pass the required status checks.

Path to main Goes through status checks?
Direct git push origin main ❌ No — the push lands immediately
Pull request with no required status checks ❌ No — the merge button is always active
Pull request with required status checks, but direct push still allowed ❌ Partial — PRs are gated, but direct pushes bypass everything
Pull request with required status checks and direct push blocked ✅ Yes — every commit on main has a successful build

Once direct pushes to main are blocked, the push trigger in the CI workflows only fires from the merge commit that GitHub creates when a PR is merged, never from a direct push — which makes it a reliable hook for post-merge steps such as tagging a release or deploying to an environment.

Status checks or pull requests: why it is not a choice

The two settings are easy to read as competing alternatives — require status checks or require a pull request — but they each close a different gap, and each one alone leaves the other gap open:

  • Required status checks alone gate the merge button, but only on pull requests. A direct git push origin main still bypasses CI entirely.
  • Require-a-pull-request alone blocks direct pushes, but with no required checks a pull request can be merged while its CI is red.

So the honest answer to “which one?” is both. Only the combination guarantees the invariant this project actually cares about — every commit on main has been built and tested — which is why the paths-to-main table marks the combination as the single ✅ row.

For a single-maintainer repository, the appropriate strength for that pull-request rule is zero required approvals: direct pushes stay blocked and CI stays enforced, yet the maintainer can still merge their own pull requests without waiting on a second person. The approval count is the lever to raise later, once the project grows into a team that practices peer review.

Keeping main green with direct pushes

Some teams deliberately allow collaborators to push straight to main — the classic Trunk-Based Development model of committing to the trunk many times a day rather than through pull requests. The question then becomes: how do you stop a direct push from breaking the build?

The uncomfortable answer is that GitHub cannot block a direct push based on CI results. Required status checks evaluate the merge button of a pull request; a direct git push origin main has no merge button — the commit lands the instant the push is accepted, before any workflow has run. There is no server-side “run the pipeline, then accept or reject the push” mechanism on GitHub-hosted repositories. Server-side enforcement is precisely what the pull-request flow gives you, and it is the only thing that guarantees a green main.

If you still want direct pushes, you keep main green by moving the check off the server to the two places that can run before or around the push:

Mechanism Enforced? When it runs Keeps main green?
pre-push git hook running the module build locally No — bypassable with git push --no-verify Before the push leaves the machine Best-effort; catches honest mistakes
CI on push + auto-revert of the breaking commit No — reactive After the commit has landed Reverts quickly, so main is red only briefly
Pull request + required status checks Yes — server-side Before the merge button unlocks Guaranteed
  • A pre-push hook runs the same checks CI runs, locally, before the push leaves the machine, and aborts it if they fail. Because it lives on the developer’s machine it is bypassable, so it depends on team discipline rather than enforcement. For how to set one up that mirrors the CI path filters, see Run a pre-push build gate.
  • Auto-revert on red does not prevent a broken main; it bounds how long main stays broken by reverting the offending commit automatically when the post-push workflow fails. This is the Trunk-Based “stop the line” safety net.

In short: direct pushes can be made safe in practice (a pre-push hook plus revert-on-red plus small, frequent commits), but only the pull-request flow makes a green main guaranteed. That trade-off — enforcement versus the zero-ceremony of pushing to the trunk — is the real decision behind allowing direct pushes.