A ctlplne studio product

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:

  1. 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.
  2. release.yml → require-green-ci — a job already in the release workflow. When you push a v* tag, it refuses to build or publish anything unless the full ci workflow 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 main before 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 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 the images and binaries jobs.
  • 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 main or a PR, so ci never 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.

Rendered live from github.com/ctlplne/probectl — found a mistake? edit this page.