Files
pwap/ops
pezkuwichain 7482421f3d ops: capture branch protection and the deploy environment as code
Both were configured by hand through the GitHub API today. Nothing in the repo
reflected them, so they were invisible here, unversioned, and removable without
a trace — which undercuts their purpose, since they exist so that nothing reaches
main or production unreviewed.

apply-repo-settings.sh is now the source of truth for both, and idempotent: each
call PUTs the full desired state rather than stacking changes. --check reports
drift without touching anything, which is what makes this useful later — a
setting silently relaxed is otherwise only discovered when it matters.

Documented alongside it: why CI Gate  is the only required check (it is an
aggregate that already inspects the others, and listing each job would mean a
rename quietly drops a requirement), and why enforce_admins stays false (an
incident needs an escape hatch — rare, visible, recorded).

This is the same class of problem as the migrations fixed earlier today: state
that only exists because someone once clicked something, with no record of what
or why.
2026-07-31 02:07:21 -07:00
..

ops

Operational configuration that lives outside the application code.

apply-repo-settings.sh

The repository's GitHub protections, as code.

Branch protection and the production environment are configured through the GitHub API. Nothing in the repository reflects them, so by default they are invisible here, unversioned, and gone without a trace if someone removes them — which undercuts the point, since both exist precisely so that nothing reaches main or production unreviewed.

./ops/apply-repo-settings.sh          # apply the desired state
./ops/apply-repo-settings.sh --check  # report drift, change nothing

Idempotent: each call PUTs the full desired state, so repeated runs converge. Requires gh authenticated with admin rights on the repo.

What it enforces

Branch protection on main

setting value why
required check CI Gate ✅ aggregate job — fails if web, backend or security-audit did not succeed
strict true branch must be up to date with main before merging
approvals 1 no unreviewed code on main
dismiss stale reviews true an approval does not carry over to new commits
force pushes blocked history cannot be rewritten
deletions blocked main cannot be deleted
conversation resolution required review comments cannot be merged past

Only CI Gate ✅ is listed as a required check, deliberately. It runs with if: always() and inspects the results of the jobs it needs, so it already covers them. Listing each job here as well would mean editing this file whenever one is renamed — and a rename would then quietly drop a requirement instead of failing loudly.

enforce_admins stays false on purpose: an admin needs a way through during a real incident. It is an escape hatch, not the normal path — rare, visible, and recorded in the branch's protection history.

production environment

Deployment approval is an environment protection rule, not something the workflow implements. GitHub holds the run until a reviewer approves it:

  • no runner is occupied while waiting
  • the window is 30 days, so a missed notification costs nothing
  • who approved which SHA is recorded in the deployment history
  • approval state survives a runner restart

The deploy jobs in quality-gate.yml opt in with environment: production. notify-deploy-pending only sends a Telegram message pointing at the run; it cannot block anything.

This replaced a job that polled /tmp/pexsec-gates for 30 minutes while holding a runner slot. On 2026-07-30 that window expired unseen and cancelled a deploy — a merged migration never reached the database and the only trace was a failed job.

Reviewer

REVIEWER_ID is a numeric user id rather than a login, because the API takes ids and a login rename would silently break the rule instead of erroring.

Override any of it via environment variables:

REPO=owner/repo BRANCH=main REVIEWER_ID=123456 ./ops/apply-repo-settings.sh