✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (
branch-protection)Validation by Beadloom
doc_sync— same source assync-check.
Branch Protection
The setup-branch-protection helper, in the onboarding domain.
Source: src/beadloom/onboarding/branch_protection.py
Specification
Purpose
Configure main for strict trunk-based development idempotently: require a PR (no direct push) and make the consolidated ci.yml check-runs required status checks, so the pipeline becomes true enforcement rather than advisory CI.
Protection contract
build_protection_payload produces the GitHub request body: required status checks in strict mode over the supplied contexts, enforce_admins: true (even admins integrate through PRs — no bypass), zero required reviews, and restrictions: null so the owner is not locked out and can still self-merge a green PR. BranchProtectionRequest captures the exact gh api call — endpoint, deterministic JSON payload, and arguments — so it is inspectable and mockable; apply_branch_protection sends it through a GhRunner.
The default set, and what may change it
DEFAULT_STATUS_CHECK_CONTEXTS holds nine contexts — the check-runs of the consolidated ci.yml (gate, the four tests (3.x) matrix legs, the two tests-locale (...) environment-dimension legs, site-build, ai-techwriter). It is the SCAFFOLDED DEFAULT that setup-branch-protection PUTs in any repository, not a description of any repository's live protection.
The set has grown and shrunk, and the shrink is the instructive one. BDL-061.39 added a tenth context, tests-windows, for a platform dimension; the owner withdrew it in beadloom-mr2l.64 on a measured cost — ~16-28 runner-minutes per pull request, and, unlike the locale legs, the pipeline's critical path, roughly tripling PR-to-merge latency for a platform outside this project's audience. A withdrawal moves the ci.yml job and this constant in ONE change, in either direction: a job with no context is a check that gates nothing, and a context with no job is a check-run that never reports.
Invariants
- A required status-check context must match a real check-run name exactly and must not be produced by a path-filtered workflow (it would not run on every PR under
strict, stalling PRs). - Every declared context is produced by a job that exists, and every job that exists is a declared context. Both directions are enforced against the real
ci.ymlbytests/self_check/config/test_ci_consolidated_structure.py::test_required_contexts_ match_ci_yml_check_runs; the second direction is what the withdrawal above exercised. enforce_admins: truewith zero required reviews — strict trunk-based, but the owner can still self-merge.- The operation is safe to re-run.
API
Module src/beadloom/onboarding/branch_protection.py:
- The default runner states
encoding="utf-8"in both directions, and the outbound one is the reason it is not cosmetic: the payload is PUT ongh's stdin, sotext=Truewould have encoded it with the image's locale — a check-run context with one non-ASCII character would raise on an ASCII image and be silently altered on an 8-bit one, i.e. the branch would be protected with a required check whose name nobody declared. JSON is UTF-8 by definition (RFC 8259 §8.1).errorsstays strict: an outbound payload must never be made lossy to keep a call alive (BDL-061.42). build_protection_payload(*, status_check_contexts=DEFAULT_STATUS_CHECK_CONTEXTS) -> dict— the GitHub branch-protection request body.BranchProtectionRequest—owner,repo,branch,status_check_contexts, withendpoint(),payload_json(), andgh_args().apply_branch_protection(...)— apply the protection via aGhRunner.GhRunner— the runner protocol (mockable in tests).
Testing
Tests: tests/integration/onboarding/branch_protection/test_branch_protection.py