Branch protection for main¶
main currently has no effective protection. Checked with the GitHub API on
2026-09-10:
- Classic branch protection on
main: none (GET …/branches/main/protection→ 404). - One active repository ruleset, "Branch rule", with deletion, non-fast-forward, code
scanning and code-quality rules, but its
ref_name.includelist is empty, so it targets no branch at all, and it requires neither a pull request nor any status check.
Every check described below runs on pull requests, but nothing enforces that they passed
before a merge: protection is the piece that turns a green CI into an actual gate. That
includes the documentation contract: Docs runs mkdocs build --strict and
make docs-contract-check, and it only stops drift once it is required.
This page is the checklist a maintainer applies in GitHub. It is deliberately explicit about check names, because a required check whose name does not match a real job blocks every merge with "Expected, waiting for status to be reported", and that failure mode is easy to misread as a broken pipeline.
Apply it¶
Settings → Branches → Add branch protection rule, branch name pattern main.
| Setting | Value | Why |
|---|---|---|
| Require a pull request before merging | on | No direct pushes to main. |
| Required approvals | 1 | A second pair of eyes on every change. |
| Dismiss stale pull request approvals when new commits are pushed | on | An approval describes the diff it was given for. |
| Require status checks to pass before merging | on | The actual gate (list below). |
| Require branches to be up to date before merging | on | Forces re-run against the merge target; catches semantic conflicts that merge cleanly. |
| Require linear history | on | Squash-merge only; keeps main bisectable. |
| Require conversation resolution before merging | on | No merging over unaddressed review comments. |
| Allow force pushes | off | |
| Allow deletions | off | |
| Do not allow bypassing the above settings | on | Applies the rule to admins too. |
Merge button configuration lives in Settings → General → Pull Requests: enable Allow squash merging only, and disable merge commits and rebase merging so "Require linear history" cannot be violated.
Required status checks¶
Names must match exactly. These are the job names as they appear on a pull request:
CI (.github/workflows/ci.yml)
LintTest (1.26.6)BuildFrontend lint and typecheckRace detectorGo vulnerability scanDB security verifyMigration up/down/upHelm StrictMode gatesSchedule multi-replicaLoad smokeRelease build smokeDocker image smokeDocs
Security (.github/workflows/security.yml)
Supply Chain BaselineCodeQL Analysis (go)CodeQL Analysis (javascript-typescript)Go Security CheckerSecret ScanningDependency Review
E2E (.github/workflows/e2e.yml)
End-to-End TestsBrowser E2E (Playwright)
PR hygiene (.github/workflows/pr-hygiene.yml)
Require linked issue in PR body
Do not require these¶
Scorecard: reportsNEUTRALon pull requests, which never satisfies a required check.Sourcery review: third-party, skipped on many runs; an outage would block all merges.CodeQL: the aggregate umbrella check. Require the two per-languageCodeQL Analysis (...)jobs instead; the umbrella can report before the analyses finish.
The Test (1.26.6) trap¶
Test is a matrix job (go-version: ['1.26.6'] in ci.yml), so its check name embeds the
Go version. Bumping the Go toolchain renames the check, and the old name stays required
and never reports; every PR blocks until someone updates the protection rule.
When changing go-version in ci.yml, update the required check name in the same change,
or drop the matrix so the job is plainly named Test.
Verify the rule works¶
After saving, open a scratch PR that deliberately fails one check (e.g. add an unformatted Go file) and confirm the merge button is blocked. A rule that exists but lists no valid required checks looks identical to a working one from the PR page.
Release workflow pins¶
.github/workflows/release.yml runs only on a tag push, so nothing in the normal PR flow
executes it. A pinned action SHA that does not exist upstream therefore stays invisible until
someone cuts a release, at which point the job fails in "Set up job" before a single step runs
and the tag produces no artifacts. This happened to v2.1.0:
actions/download-artifact was pinned to a SHA that was not in the action's repository.
The Lint job resolves every pinned SHA in .github/workflows/*.yml on every PR, including
multi-segment action paths such as github/codeql-action/init@<sha>. Keep that step required,
and when Dependabot is told to ignore an action, re-pin it by hand to a real commit for the
version tag you intend, not to a SHA copied from elsewhere.
The check fails only on HTTP 404/422, which mean the commit genuinely is not in the action's
repository. A rate limit, a GitHub API incident or a network failure is retried and then
reported as UNKNOWN without failing: this step is a required check, and a check that treats
every API hiccup as a bad pin would block every open PR for the duration of an upstream
outage.
When CI gains a job¶
New blocking jobs must be added here and to the protection rule; a job that runs but is
not required is advisory only. A job must not be added to the rule before it exists on
main, for the "waiting for status" reason above: every open PR would block on a check
that never reports.
Related¶
- RELEASING.md: the release gate these checks feed
- Deployment model