trstctl /docs GitHub ↗

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/CODEOWNERS and .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) and docs/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

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