Branch protection & required checks (codified)
This page is the human-readable companion to the in-repo branch-protection policy.
It explains which checks must pass before anything merges to main, who must
review which paths, and how an admin applies and verifies the rules — so the gate is
provable from the repository, not an invisible server-side setting.
Why this exists. The audit (TEST-006) flagged that "blocks merge" depended on a repo admin having configured required checks / enforce-admins / linear-history server-side — invisible to the repository and to a reviewer. A job that runs but is not required is theater: a red build could merge, and an admin could force-push, with no in-repo trace. Codifying the policy here (plus
.github/CODEOWNERSand.github/branch-protection.json) makes the gate auditable. Two reality-tests keep the codified policy honest:docs/codeowners_test.go(every security-critical path is owned) anddocs/branch_protection_test.go(the required-check list matches the real CI job names).
The policy for main
The canonical, machine-applicable form lives in
.github/branch-protection.json.
In words, merging to main requires:
- All required status checks green (and the branch up to date —
strict). The required set is every CI gate plus the security scans (see the table below). A check that runs but is not in this list does not block merge; a check in this list that is missing/failing does. - At least one approving review, and — for the root-of-trust paths —
approval from a code owner (
require_code_owner_reviews). Stale approvals are dismissed on a new push (dismiss_stale_reviews), and the last push must itself be approved (require_last_push_approval), so a sneak-in commit after approval cannot ride in. - Linear history (
required_linear_history) — squash/rebase, no merge commits. - No force-pushes and no branch deletion (
allow_force_pushes: false,allow_deletions: false) — history cannot be rewritten under the protection. - Enforce on admins (
enforce_admins: true) — maintainers are bound by the same rules; nobody bypasses the gate. - Conversation resolution required before merge.
Required status checks
These are the exact GitHub check names (the name: of each CI job). They are kept in
sync with the workflows by docs/branch_protection_test.go, which fails if a job is
renamed or removed without updating the required-check list — so a gate can never
silently fall out of the required set.
| Check (job name) | Workflow | What it guards |
|---|---|---|
build / test / lint |
ci.yml |
Build all 3 binaries, make test (race + coverage floors), make lint (gofmt/vet/trstctllint AN-1/3/5/8), gate self-tests |
web ui (typecheck / test / build) |
ci.yml |
Web console typecheck, Vitest + axe, Vite build, npm SCA |
docs site (mkdocs build --strict) |
ci.yml |
Docs build with no broken nav/links |
actionlint (workflow lint) |
ci.yml |
Workflow + shell lint of the pipelines themselves |
govulncheck |
ci.yml |
Reachability-aware vulnerability scan |
supply-chain (SBOM + binary SCA) |
ci.yml |
Module SBOM + embedded-Postgres provenance/scan |
helm (lint + render + schema) |
ci.yml |
Control-plane chart lint + kubeconform |
proto (buf lint + breaking-change gate) |
ci.yml |
Signer gRPC contract (AN-4) wire-compat |
acme conformance (Pebble differential) |
ci.yml |
ACME protocol differential vs the reference CA |
windows cross-build |
ci.yml |
Whole module cross-compiles for Windows |
windows / test + MSI |
ci.yml |
Windows agent surface (real cert store) + MSI |
kubernetes / kind e2e |
ci.yml |
In-cluster e2e + cert-manager bridge |
secret scan (gitleaks) |
security.yml |
No committed secrets |
container image scan (Trivy) |
security.yml |
Image vulnerability scan |
CodeQL (codeql.yml) also runs on every PR; because its check name is a build-matrix
template (analyze (<language>)) rather than a fixed string, it is recommended as a
required check but is configured in the GitHub UI rather than pinned by literal name
here (the sync-test deliberately omits matrix-expanded names so it stays robust).
Release-time gate
A version tag does not ship off an unverified commit: release.yml re-runs the
full suite (make build + make test) against the exact tagged ref in a test
job, and every build/sign/publish job needs: test (TEST-005). So even a tag placed
on a commit whose PR checks were skipped cannot publish a signed artifact without the
suite going green on that ref.
Code ownership
.github/CODEOWNERS
assigns mandatory reviewers. The security-critical paths — the AN-3 crypto boundary
(internal/crypto), the AN-4 isolated signer (internal/signing, cmd/trstctl-signer,
proto), the AN-1 multi-tenant store (internal/store), and the architecture linter
that enforces the guardrails in CI (tools/trstctllint) — are owned explicitly, so
with require_code_owner_reviews enabled no change to the root of trust merges
without a security review. docs/codeowners_test.go asserts each of these paths stays
covered.
Apply it (repo admin)
# Apply the codified protection to main (requires admin on the repo):
gh api -X PUT repos/imfeelingtheagi/trstctl/branches/main/protection \
-H "Accept: application/vnd.github+json" \
--input .github/branch-protection.json
# Or manage it as code with Terraform's github_branch_protection resource, mirroring
# the same contexts / enforce_admins / linear-history / code-owner-review settings.
Verify it (anyone with read on the API)
# The applied protection should match the codified policy (required checks,
# enforce-admins, linear history, code-owner review).
gh api repos/imfeelingtheagi/trstctl/branches/main/protection | jq '{
contexts: .required_status_checks.contexts,
enforce_admins: .enforce_admins.enabled,
linear: .required_linear_history.enabled,
code_owner_reviews: .required_pull_request_reviews.require_code_owner_reviews
}'
If the applied protection and .github/branch-protection.json ever diverge, the
in-repo file is the intended policy; re-apply it.
See also
Supply chain & build integrity ·
Vulnerability management ·
SECURITY.md