Branch protection for main and release gating
What this is
CI runs a long list of checks on every pull request (PR — a proposed
change, reviewed before it lands). By themselves those checks are only
advisory — GitHub will happily let you click "Merge" on a red PR unless you
tell it not to. CI is an inspector who files reports; branch protection is
the rule that a red report locks the door. Without it, the reports pile up and
the door opens anyway. This page is how you make the checks blocking — a
required status check is one whose green result the merge button refuses to
work without — so a change cannot land on main until CI is green.
There are two independent layers, and you want both:
- Branch protection on
main— a GitHub repository setting (not something in this repo's code), configured once in the web console. It is what makes the merge button refuse a red or out-of-date PR. Because it is a server-side setting, the repo cannot turn it on for you; an admin must. release.yml→require-green-ci— a job already in the release workflow. When you push av*tag, it refuses to build or publish anything unless the fullciworkflow concluded green on that exact commit. This is a second, independent backstop: it holds even for a tag cut off a side branch, or by an admin who bypassed branch protection.
Layer 1 guards the merge; layer 2 guards the release. Neither depends on the other.
There is no CI job that verifies branch protection — it cannot be checked from inside the repo, because the setting lives in GitHub's server config, not in the tree. Auditing it is a manual console check (below).
Turn on branch protection (one-time console steps, ~5 minutes)
GitHub → repository → Settings → Branches → Add branch protection rule:
- Branch name pattern:
main - Enable Require status checks to pass before merging
- Enable Require branches to be up to date before merging (forces a PR to
re-run CI against the current tip of
mainbefore it can merge — catches "passed in isolation, breaks after someone else's merge"). - Required checks — add the check(s) from the next section. A check that is not listed here is advisory again, so this list is the whole point of the rule.
- Enable Require branches to be up to date before merging (forces a PR to
re-run CI against the current tip of
- Enable Require a pull request before merging — set the approval count to fit the team (a solo maintainer can leave approvals at 0; the required status checks still block the merge button).
- Enable Do not allow bypassing the above settings (a.k.a. "Include administrators"). Without this, every gate above is advisory for admins — which defeats the purpose.
- Leave Block force pushes and Block deletions on (they are defaults of the rule, and they stop history from being rewritten out from under the gates).
Then Settings → Tags → New rule (tag protection): pattern v*, so only
maintainers can create release tags. (Even without this, require-green-ci
refuses to release an untested commit — but tag protection stops an accidental
tag in the first place.)
Which checks to require
The lowest-maintenance required-check set is verify-all + the two PR-only
commit-policy jobs. verify-all is an umbrella job in ci.yml — it runs
no tests itself; it needs: (declares a dependency on) the always-running
verification suite and fails red if any of them is red or skipped. Treating
skipped as failure is deliberate fail-closed behavior: a gate that quietly
didn't run looks exactly like a gate that passed unless something forces the
distinction.
Three jobs intentionally run outside the verify-all umbrella because they are
PR-only: they are skipped on direct pushes to main, and verify-all would
correctly treat that skip as red. Require the two commit-policy gates explicitly
in branch protection; coverage-comment is a best-effort, no-checkout
notification job rather than a merge gate:
| Required check | Gate it enforces |
|---|---|
verify-all |
umbrella — fails unless every gate in its needs: list is green |
commitlint |
Conventional Commits on PR commits |
dco |
Developer Certificate of Origin sign-off |
If your organization's policy instead requires listing every job by name (some
auditors prefer the explicit list), the complete set of top-level ci.yml
jobs is below — 42 specialist jobs plus the verify-all umbrella, for
43 top-level jobs in the workflow. Keep the list in sync with the workflow —
a job you forget to list is advisory again, so prefer the verify-all plus
commitlint/dco approach unless you have a reason not to.
| Required check | Gate it enforces |
|---|---|
action-pins |
every workflow action is pinned to a commit SHA |
secret-scan |
no committed secrets across current files or git history (gitleaks) |
no-devauth-in-release |
the eval-only dev auth mode cannot ship in a release build |
lint-go |
gofmt + go vet + golangci-lint |
lint-python |
ruff + black (the BGP analyzer) |
editions-gate |
core never imports ee/; the core-only build stays green (see editions.md) |
packaging-smoke |
install/package smoke checks |
fips-gate |
the FIPS artifact builds; the validated crypto module is active (see hardening.md) |
test-go |
unit tests, fuzz smoke, cross-compile, endpoint cross-OS |
device-live |
live SNMP/device telemetry smoke |
path-raw-live |
live raw-socket path probe smoke |
rca-eval |
AI root-cause-analysis quality eval |
coverage |
per-package coverage floor |
coverage-comment |
PR-only best-effort coverage comment; no checkout or repository-code execution |
test-python |
BGP analyzer tests |
browser-worker |
Playwright worker real-browser smoke |
openapi-gate |
no undocumented core/provider routes and no undocumented CLI-parity gap |
completeness-gate |
every release-catalog capability has validated end-to-end wiring evidence, an explicit none-by-design disposition, or a visible partial-evidence gap that remains outside the coverage count |
delivery-audit-gate |
signed exact-source receipt policy, out-of-band auditor trust, repository-only tool boundary, and planted fixture-only rejection |
sdk-gate |
generated REST SDKs are in sync with OpenAPI |
migration-gate |
expand/contract (zero-downtime) migrations |
helm-gate |
Helm chart lints + hardening invariants; GitOps manifests + compose config valid |
terraform-gate |
terraform fmt + validate |
ebpf-kernel-matrix |
eBPF programs load across the supported kernel matrix |
ebpf-image-live |
the shipped eBPF-agent image is the live (not fixture-replay) build |
cross-tenant-isolation |
permanent RLS tenant-isolation gate — never remove (cross-tenant leakage is the highest-severity failure; see isolation.md) |
integration |
real Kafka/Postgres/ClickHouse stack |
perf-smoke |
ingest-path performance floor |
backup-drill |
backup → restore drill survives (see backup-restore.md) |
failover-drill |
timed Postgres failover drill (see dr.md) |
chaos-dependency-drill |
dependency chaos recovery drill |
load-smoke |
load/soak smoke |
proto |
buf lint + breaking-change check |
web |
typecheck, eslint, npm audit, surface-coverage + jsdom a11y + tests |
web-rendered-a11y |
rendered route/theme/viewport accessibility plus J1–J6 LCP/INP performance gate |
dependency-scan |
govulncheck / npm / pip advisories |
build-images |
the release Dockerfiles build |
image-scan |
Trivy image scan |
commitlint |
Conventional Commits |
dco |
Developer Certificate of Origin sign-off |
sbom |
release SBOM (SPDX) generates |
compose-render |
all shipped compose files render |
verify-all |
executed-verification umbrella |
If you require verify-all, you do not also need to list the jobs it
already covers — listing them is redundant (though harmless). You do still
need commitlint and dco because they are PR-only and intentionally outside
the umbrella.
How the release gate works (release.yml)
A tag is a named pointer to one exact commit — v1.2.3 names the precise
source a release is built from. Pushing a v* tag does not trigger ci (tag
pushes and branch pushes are different events), so the release workflow can't
just "wait for its own CI". The require-green-ci job instead looks up the
ci run for the tagged commit via the GitHub Actions API
(ci.yml/runs?head_sha=<the tag's commit>):
- completed + success → the tag workflow runs the strict
completeness-release-gate; only a 100%-covered ledger unlocks theimagesandbinariesjobs. - completed + failure/canceled → the release fails; nothing is built.
- still running → it polls for up to ~30 minutes (60 tries, 30 s apart), then proceeds or fails based on the outcome.
- no run at all (the commit was never pushed to
mainor a PR, socinever ran on it) → the release fails with instructions.
So the only path to a release is: push to main → ci goes green on that
commit → the strict capability ledger has zero gaps →
git tag -a vX.Y.Z -m "..." && git push origin vX.Y.Z. A tag on an untested,
red, or incomplete commit produces no artifacts, with or without branch
protection.