$ cd ../writing

githubplatformgovernance· 2026-07-10· 5 min

Gatekeeper: one required check to rule them all

We deleted the pipeline checks from branch protection and merging got stricter. A convention-based PR gate for when your checks run outside GitHub, and the seven design decisions behind it.

We deleted the pipeline checks from our branch protection.

Merging got stricter.

Both of those are true, and the gap between them is a design problem worth walking through.

The setup: checks in two worlds

Our pipelines run in Azure DevOps. Our pull requests live in GitHub. Check names differ per repository and per pull request. Terraform validation only runs when infrastructure actually changed.

GitHub’s branch protection wants a static list of required check names. That list has to be correct for every pull request in every repository, forever.

It cannot be.

The native gap

GitHub has no way to express “required only when it runs”. Three specific failure modes follow from that.

A required check that gets skipped blocks the pull request forever. Not fails. Blocks. The check never reports, branch protection waits for a report that will never come, and the merge button stays grey until somebody with admin rights overrides it.

A conditionally skipped job reports “Success”. That is arguably worse, because it is a false green. The job did not run, and the gate is satisfied.

A static required-checks list rots. Every pipeline rename, every new validation stage, every repository that does things slightly differently is a manual edit in a settings page nobody remembers to make.

So the list is gone. One check replaced all of them.

Decision 1: one gate, not N checks

Branch protection now lists a single pipeline check: Gatekeeper. It decides what must pass on this pull request.

PR opens
  → Gatekeeper reads defaults.yaml + the repo's .github/gatekeeper.yaml
  → matches running checks by convention: CI*, PRA*, PRV*, TF*
  → waits (with a timeout), posts a live status table into the PR
  → goes green only when every matched check passes

Two checks or twenty-seven, it does not matter. The gate discovers what is actually running and holds the merge until those specific checks pass.

Decision 2: convention over configuration

The org-wide conventions live in one defaults.yaml:

required_prefixes:
  - prefix: CI    # Continuous Integration
  - prefix: TF    # Terraform Plan
  - prefix: PRA   # Pull Request Administration
  - prefix: PRV   # Pull Request Validation

Name a pipeline to convention and it becomes required automatically. No config edit, no settings page, no ticket. The naming pattern is the registration.

This is the same bet behind how we run GitHub teams, repositories, and Renovate across 100+ repositories. Convention removes the per-repository conversation, which was always the expensive part.

Decision 3: block on failures, not on absence

This is the decision that fixes GitHub’s skipped-check trap:

acceptable_conclusions:
  - success
  - skipped   # e.g. path filter, nothing changed
  - neutral   # informational, non-blocking

Terraform validation did not run because no infrastructure changed? That is a correct outcome, not a missing one. All three conclusions pass the gate.

The distinction that matters: Gatekeeper blocks on a check that failed, not on a check that did not need to happen. GitHub’s model cannot tell those apart. That is the whole reason this exists.

Decision 4: fail fast on ghosts

A check that never starts should not cost half an hour:

timeout: 30                # pending too long
missing_check_timeout: 5   # expected, never appeared

Two timeouts, deliberately different. A check that is running but slow gets thirty minutes. A check that was expected and simply never appeared gets five, because waiting longer will not summon it. Most gates have one timeout, and it is always tuned for the wrong case.

Decision 5: defaults should make per-repo config nearly empty

Everything above comes from defaults. A repository adds only what is extra. Here is an entire per-repository config:

required_checks:
  - pattern: '* / Validate PR Title Format'
  - pattern: 'Aikido Security: check code'

That is the whole file.

If your platform tool requires a fifty-line config per consumer, the defaults are wrong. The measure of a good default is how little the common case has to say.

Decision 6: the gate ignores itself

for check in required:
    if check is self: continue

Gatekeeper is itself a check running on the pull request. Without explicit self-exclusion it matches its own name, waits for itself to pass, and never opens.

Obvious in hindsight. Not obvious at three in the afternoon on the first test run.

There is a related edge worth knowing: an external review app that runs long can block or confuse the gate. Which is exactly why the ignore rules are explicit rather than clever.

Decision 7: show your work

Gatekeeper posts a live status table into the pull request: every matched check, why it is required, and its current state.

CHECK REQUIRED BECAUSE STATUS
CI · Build prefix: CI* ✓ success
TF · Plan prefix: TF* − neutral
PR Title Format config ✓ success
Aikido Security config ✓ success

A gate you cannot inspect is a gate you fight. When somebody asks “why is my PR blocked”, the answer is in the pull request, not in a pipeline log they need permissions to read.

The “required because” column is the one that stopped the questions. It turns the gate from a verdict into an explanation.

Rollout: shadow mode first

It did not start by blocking anything. First it only commented. We watched. It started enforcing only when the comments stopped surprising us.

That sequencing is not caution for its own sake. A gate that blocks incorrectly on day one teaches everyone to route around gates, and you do not get that trust back cheaply.

Today it guards 19 repositories through one shared workflow.

The pattern underneath

Branch protection lists one pipeline check. The conventions decide everything else.

That inversion, from “enumerate what is required” to “describe how to recognize what is required”, is the reusable part. The specific YAML matters much less than the shift from a list somebody maintains to a rule the system applies.


Originally published on LinkedIn, July 2026, in two parts. Consolidated here.