Branch Protection Rules

Recommended GitHub branch protection rules to encode ADR-0001 (solo operator with disciplined review). GitHub doesn't store these declaratively by default — this doc captures what to configure via the UI or `gh api`.

Branch protection rules don’t ship as YAML in the repo (by default — safe-settings etc. exist but add operational burden). This doc captures what to set up via GitHub UI or gh api per-repo, encoded so future-you doesn’t relitigate the choices.

What these rules do

Branch protection rules enforce the ADR-0001 workflow at the platform level: PR required, CI green before merge, code-owner review required for protected paths. The rules below are the recommended baseline for every Abukix repo.

Where rules live

Per repo, on the main branch. GitHub doesn’t reconcile branch protection from repo files by default — you configure them once per repo via:

  • GitHub UI: Settings → Branches → Branch protection rules → Add rule
  • gh api: programmatic, scripted (recipe below)
  • safe-settings GitHub app: declarative YAML reconciliation — adds operational burden; skip until you have multiple repos to maintain

Public OSS repo (basecamp, triage, terralabs, platform-ctl, data-tier, llm-gateway, mcp-servers, aiops, studio, rxp, pulse)

Branch name pattern:  main

✅ Require a pull request before merging
   ✅ Require approvals: 1
   ✅ Dismiss stale pull request approvals when new commits are pushed
   ✅ Require review from Code Owners
   ⬜ Restrict approvals to users with write access  (off — keeps it open for external)
   ⬜ Allow specified actors to bypass required pull requests

✅ Require status checks to pass before merging
   ✅ Require branches to be up to date before merging
   Required status checks:
     - ci-check / yaml-lint
     - ci-check / gitleaks
     - ci-check / kustomize-build       (basecamp only)
     - ci-check / flux-validate         (basecamp only)
     - ci-check / kyverno-test          (basecamp only)
     - ci-check / go-test               (Go projects only)

✅ Require conversation resolution before merging
✅ Require signed commits
✅ Require linear history       (cleaner Git history; rebase or squash)

⬜ Require deployments to succeed before merging   (skip — Flux handles)

✅ Lock branch                   (off — only enable if archiving the repo)
⬜ Do not allow bypassing the above settings    (you'll need to bypass occasionally)
⬜ Restrict who can push to matching branches  (off for solo; on if co-maintainers join)

✅ Allow force pushes — Specify who can force push:  (limited to admin: yourself)
✅ Allow deletions — Specify who can delete:          (limited to admin: yourself)

Private repo (ops-handbook)

Same as public, BUT:

  • Skip the kustomize-build / flux-validate / kyverno-test checks (ops-handbook isn’t a K8s repo).
  • Skip the go-test check.
  • Keep gitleaks + yaml-lint + /pre-publish-check if any content might eventually be sanitized + published.

Solo-operator caveat

GitHub will not let the same account that opened a PR be the approving reviewer. With one human operator, the “Require approvals: 1” rule cannot be satisfied by you alone on PRs you open. Three workable resolutions:

  1. Bypass yourself for routine work (the box “Do not allow bypassing the above settings” stays unchecked). The CI green + overnight wait + (consequential cases) external review carries the safety load instead. This is the ADR-0001 recommendation.
  2. Use a GitHub App or bot for approvals on routine work (e.g., a static-analysis bot that approves when its checks pass). Requires setup; useful at Y3+ scale.
  3. Drop the “Require approvals” rule entirely and rely on CI + CODEOWNERS-driven “Require review from Code Owners” for paths that truly need it. CI does the load-bearing work.

The /root recommendation is option 1 — keep the rule visible as aspirational (it’ll be satisfied when contributors join), and rely on CI + overnight + external-review-when-consequential for now.

Apply via the GitHub CLI (rerun is idempotent — it PUTs the full rule):

REPO="<your-handle>/basecamp"   # or any other Abukix repo

gh api -X PUT \
  "/repos/${REPO}/branches/main/protection" \
  -F required_status_checks[strict]=true \
  -f required_status_checks[contexts][]="ci-check / yaml-lint" \
  -f required_status_checks[contexts][]="ci-check / gitleaks" \
  -f required_status_checks[contexts][]="ci-check / kustomize-build" \
  -f required_status_checks[contexts][]="ci-check / flux-validate" \
  -f required_status_checks[contexts][]="ci-check / kyverno-test" \
  -F enforce_admins=false \
  -F required_pull_request_reviews[required_approving_review_count]=1 \
  -F required_pull_request_reviews[dismiss_stale_reviews]=true \
  -F required_pull_request_reviews[require_code_owner_reviews]=true \
  -F required_linear_history=true \
  -F required_signatures=true \
  -F allow_force_pushes=true \
  -F allow_deletions=true \
  -F required_conversation_resolution=true \
  -F restrictions=null

Adjust the contexts[] list per-repo: drop the K8s-specific ones for non-basecamp repos; add go-test for Go projects.

Verification

After applying, confirm:

gh api "/repos/${REPO}/branches/main/protection" | jq '.required_status_checks, .required_pull_request_reviews'

You should see the rules you set.

Then open a test PR (in a draft branch) and verify CI fires.

Re-verification cadence

Re-verify these rules on:

  • Every new Abukix repo bootstrap (apply the recipe above).
  • Annual ADR review (revisit ADR-0001’s annual-revisit follow-up).
  • When a co-maintainer joins — likely supersedes ADR-0001 and tightens these rules.

Anti-patterns

Anti-patternWhy
Disabling branch protection “temporarily” to land a hotfixThe discipline is what catches the bad hotfix. Use a --admin push as the explicit, audited bypass instead.
Adding bypass actors silentlyEach bypass is a future debugging session asking “who pushed past the gates and why.” Make it a PR comment + ADR if you make it permanent.
Setting required reviewers but allowing self-approval via app tokenTheater. CI does the load-bearing work; don’t pretend the human gate is real if it isn’t.
Letting the required-status-checks list rotWhen CI workflows change names (rename a job), update the required list. Stale required-checks list = “always failing” PRs that confuse contributors.

Cross-references