Repository admin settings runbook¶
Single source of truth for the GitHub-side admin state that the dev-loop workflows depend on. The CI workflows, branch protection, and auto-revert all rely on settings that live outside the repository tree, so they need to be documented here so they can be re-applied after a transfer, restored after accidental drift, or reviewed during incident triage.
This document tracks current state. Each change is also captured in a dated
record under _project/decisions/ for audit history; this file is the
“what is currently configured” view.
Required workflow permissions¶
default_workflow_permissions: read
can_approve_pull_request_reviews: true
The default token permission is intentionally read-only. The
develop-post-merge.yml auto-revert-on-failure job declares
contents: write, issues: write, pull-requests: write at the job level
to scope writes narrowly. can_approve_pull_request_reviews: true lets the
workflow request review on the auto-revert PR from the original PR author
when the GraphQL lookup populates the field.
Verify:
gh api repos/joeharris76/BenchBox/actions/permissions/workflow
Apply (admin only):
gh api -X PUT repos/joeharris76/BenchBox/actions/permissions/workflow \
-f default_workflow_permissions=read \
-F can_approve_pull_request_reviews=true
History:
Toggled
can_approve_pull_request_reviewsfromfalsetotrueon 2026-04-30 during Step 4 implementation. Recorded in_project/decisions/dev-loop-auto-revert-fork-test-2026-04-30.md.
Branch ruleset — develop¶
Ruleset name: develop-squash-only (id 15611785), targets
refs/heads/develop.
Required status checks:
- ci-required-result
ci-required-result is the umbrella job in .github/workflows/pr.yml
that aggregates the required-lane jobs: ci-paths, content-guard,
code-lint, code-test, correctness-gate, plan-capture-gate,
explorer-tokens, audit-sha, package-smoke, and dependency-audit.
Branch protection deliberately keys off the umbrella so the path-aware
classifier can skip subordinate jobs without making the protected check
disappear. The classifier fails closed: any path not on the
safe-content allowlist in .github/path-filters.yml (including unknown
top-level paths) routes through code-lint + code-test.
Other ruleset properties to preserve:
strict_required_status_checks_policy: false
required_linear_history: true
non_fast_forward: true
required_pull_request_reviews: squash-only PRs
deletion: blocked
bypass_actors: (none)
Verify:
gh api repos/joeharris76/BenchBox/rulesets/15611785 --jq '
{
target: .target,
enforcement: .enforcement,
bypass_actors: [.bypass_actors[]?.actor_type],
required_checks: [
.rules[]
| select(.type == "required_status_checks")
| .parameters.required_status_checks[]?.context
],
strict_base: (
.rules[]
| select(.type == "required_status_checks")
| .parameters.strict_required_status_checks_policy
),
linear_history: any(.rules[]; .type == "required_linear_history"),
non_fast_forward: any(.rules[]; .type == "non_fast_forward"),
deletion: any(.rules[]; .type == "deletion")
}'
Soundness-path review enforcement (pending admin action)¶
The develop-squash-only ruleset’s pull_request rule must require a code-owner
review so a PR touching the CODEOWNERS-owned soundness paths cannot be
squash-merged with zero approvals. The authoritative path list is
SOUNDNESS_PREFIXES in _project/scripts/auto_merge_soundness_paths.py
(mirrored 1:1 into .github/CODEOWNERS); as of soundness-surface-widening
it covers the comparators/parsers (benchbox/core/equivalence/**,
benchbox/core/query_plans/parsers/**, benchbox/core/**/validation.py),
the oracle-adjacent surface (benchbox/core/expected_results/**,
benchbox/platforms/base/result_capture.py, the benchbox/sql_compat/
rule-dispatch core), and the review-gate machinery itself
(_project/scripts/auto_merge_soundness_paths.py,
.github/workflows/auto-merge-on-open.yml, .github/workflows/release.yml).
The self-protection entries matter because in-workflow checks are
attacker-controlled for same-repo PRs (GitHub runs the PR’s own copy of a
pull_request-triggered workflow); this CODEOWNERS + ruleset layer is the
durable control. The auto-merge code side already withholds auto-merge
enablement for those paths, but withholding is only a precondition — without
this ruleset requirement a soundness-path PR can still be squash-merged
manually. Tracked by the auto-merge-review-gate-admin-enforcement TODO.
Target state:
require_code_owner_review: true
required_approving_review_count is deliberately NOT part of the target state:
GitHub’s pull_request rule applies that count to EVERY PR against the branch, not
just CODEOWNERS-matched paths, so requiring >= 1 would gate every develop PR
instead of just soundness-path ones — defeating the fast squash-auto-merge default.
require_code_owner_review: true is independently sufficient: GitHub still requires
an owner’s approval on a PR that touches a CODEOWNERS-matched soundness path
regardless of the count setting.
Live state at this writing: require_code_owner_review: false — not yet enforced.
Applying it is a deferred repo-admin action: the develop-PR GITHUB_TOKEN has no
administration scope, so it can neither read nor write the ruleset; only an admin
PAT can.
As of ruleset-drift-check-review-rule-coverage, this predicate is now wired into
scripts/ruleset_drift_check.py’s compare_ruleset (see “Ruleset drift is checked
by scripts/ruleset_drift_check.py” below) — the daily release-canary.yml run and
validate-main-pr.yml’s bootstrap invocation both surface a missing
require_code_owner_review as a finding automatically, using the
RULESET_DRIFT_TOKEN those workflows already fetch rulesets with. Decision:
WARN-until-enforced — while the live state above says false, that finding is
prefixed WARNING (non-blocking): in the canary’s findings/JSON output and does
not fail the canary or the exit code (scripts/ruleset_drift_check.py’s
DEVELOP_REVIEW_RULE_ENFORCED = False). This avoids the canary going permanently
red before the admin PUT below lands. Once this section’s “Live state” is updated
to require_code_owner_review: true, flip DEVELOP_REVIEW_RULE_ENFORCED to True
(a one-line change) so the same gap starts failing the canary if it ever regresses.
The manual, one-off verify command below still works standalone (e.g. to check current live state without waiting for the next canary run):
gh api repos/joeharris76/BenchBox/rules/branches/develop \
| uv run --project _project/scripts -- python _project/scripts/ruleset_review_enforcement.py --rules-file -
The predicate exits non-zero and names the offending field while review enforcement
is missing, and exits zero once the ruleset requires a code-owner review. Its pinned
logic is guarded by tests/unit/release/test_ruleset_review_enforcement.py, and the
merged drift-checker behaviour (WARN-until-enforced, develop-only scoping) is
guarded by tests/unit/release/test_ruleset_drift_review_coverage.py, both in the
required fast lane.
Apply (admin only — the deferred action that closes the gap):
# Fetch the current ruleset, set the pull_request rule parameter
# require_code_owner_review: true
# then PUT it back.
gh api repos/joeharris76/BenchBox/rulesets/15611785 > /tmp/develop-ruleset.json
# edit /tmp/develop-ruleset.json (pull_request rule parameters), then:
gh api -X PUT repos/joeharris76/BenchBox/rulesets/15611785 --input /tmp/develop-ruleset.json
After applying, re-run the Verify command (expect exit 0) and confirm a zero-approval soundness-path PR can no longer be squash-merged.
History:
Switched required status check from
["lint", "test (ubuntu-latest, 3.12)"]to["ci-required-result"]on 2026-04-30 as Step 3a w7. Recorded in_project/decisions/dev-loop-path-filter-smoke-test-2026-04-30.mdwith full before/after JSON.
Branch ruleset — main¶
Ruleset name: main-release-only, targets refs/heads/main.
Release-only branch. Direct pushes are not allowed; releases land via the
release-cut / release-finalize Make targets documented in
release-guide.md.
Required status checks:
- validate-base
- release-required-result
validate-base is the branch-shape guard in
.github/workflows/validate-main-pr.yml. It allows only release branches
matching vX.Y.Z with an optional suffix.
release-required-result is the umbrella job in .github/workflows/test.yml
for release PR correctness. It aggregates the required fast lane, the bounded
real-result correctness gate (make test-correctness-gate), credential-free
integration-not-slow suite, isolated exact-one-wheel package smoke, dependency
upper-bound checks, and release-branch curation checks. It is the ruleset
context maintainers should use instead of individual matrix job names such as
test-package (...).
Other ruleset properties to preserve:
strict_required_status_checks_policy: false
required_linear_history: true
non_fast_forward: true
deletion: blocked
bypass_actors: (none)
Verify (ruleset id varies; list and inspect):
gh api repos/joeharris76/BenchBox/rulesets --jq '.[] | {id, name, target}'
gh api repos/joeharris76/BenchBox/rulesets/<main-ruleset-id> --jq '
{
target: .target,
enforcement: .enforcement,
bypass_actors: [.bypass_actors[]?.actor_type],
required_checks: [
.rules[]
| select(.type == "required_status_checks")
| .parameters.required_status_checks[]?.context
],
strict_base: (
.rules[]
| select(.type == "required_status_checks")
| .parameters.strict_required_status_checks_policy
),
linear_history: any(.rules[]; .type == "required_linear_history"),
non_fast_forward: any(.rules[]; .type == "non_fast_forward"),
deletion: any(.rules[]; .type == "deletion")
}'
If live GitHub ruleset state differs from this runbook, update the ruleset or
this document before relying on release-required enforcement. Do not treat green
validate-base and release-required-result workflow runs as mandatory unless
the ruleset also requires both contexts.
Tag creation and release-environment protections¶
Tracked by the release-tag-publish-hardening TODO. .github/workflows/release.yml
now has a verify-tag-on-main job that asserts the triggering ref (tag push
or workflow_dispatch) is main or an ancestor of it before build/publish
run, and the real-PyPI Publish to PyPI step additionally requires
startsWith(github.ref, 'refs/tags/v'). Those are workflow-level (tooling)
controls. The two sections below are the remaining repo-admin-layer half of
the fix — “who can create a v* tag at all” and “does the pypi
environment require a human approval” — neither of which the develop-PR
GITHUB_TOKEN can read or write (no administration scope), same
constraint as the soundness-path review enforcement gap above.
Tag creation restricted to release flow (pending admin action)¶
Target state: a GitHub tag-protection ruleset targeting refs/tags/v* that
restricts tag creation to the release automation identity / specific
maintainer actors, mirroring how develop-squash-only restricts pushes to
refs/heads/develop. Today, verify-tag-on-main stops a stray tag from
reaching build/publish only if the tagged commit isn’t on main — it does
nothing to stop someone with push access from creating a v* tag on a
main commit out of band (e.g. re-tagging an old main commit, or tagging a
version out of sequence). A tag-creation ruleset is the layer that closes
that gap by restricting who may create the tag in the first place.
Verify (list rulesets and look for one whose conditions.ref_name.include
targets refs/tags/v* with target: "tag"):
gh api repos/joeharris76/BenchBox/rulesets --jq '.[] | {id, name, target, conditions}'
Live state at time of writing (2026-07-03): no ruleset with target: "tag"
exists. The only ruleset whose name resembles this (v-release-branches-minimal,
id 15611787) targets branches matching refs/heads/v*, not tags — it is
unrelated to tag creation. Any collaborator with push access can create a
v* tag today.
Apply (admin only):
gh api -X POST repos/joeharris76/BenchBox/rulesets \
-f name='v-tag-restricted' \
-f target='tag' \
-f enforcement='active' \
-f 'conditions[ref_name][include][]=refs/tags/v*' \
-f 'conditions[ref_name][exclude][]=' \
-f 'rules[][type]=creation'
# Restricting the actor list further (e.g. to a release-bot identity) needs
# a bypass_actors / rules payload tailored to who should retain the ability
# to tag; draft that with the admin before applying, this is a starting
# point, not the final payload.
Why this can’t be applied by the write-task’s own PR: same as the
soundness-path section above — the develop-PR GITHUB_TOKEN has no
administration scope to read or write repository rulesets.
Drift detection (landed 2026-07-05, tag-and-pypi-environment-admin-hardening
w3): _project/scripts/ruleset_review_enforcement.py now carries a
tag_protection_findings() predicate and a TAG_RULESET_ENFORCED
warn-until-applied flag, mirroring ruleset_drift_check.py’s
DEVELOP_REVIEW_RULE_ENFORCED. Feed it the live tag rulesets to check:
# Fetch each ruleset in full (the list endpoint omits conditions/rules,
# which the predicate correctly treats as "not protected"):
ids=$(gh api repos/joeharris76/BenchBox/rulesets --jq '.[].id')
for id in $ids; do gh api repos/joeharris76/BenchBox/rulesets/$id; done \
| jq -s '.' \
| uv run -- python _project/scripts/ruleset_review_enforcement.py --rulesets-file -
While TAG_RULESET_ENFORCED is False (until the POST above lands), a
missing/incomplete v*-tag ruleset prints as WARNING (non-blocking): and
exits 0 — the check ships before the admin acts without going red. The
predicate flags a ruleset that is not active; whose ref_name.include
does not cover refs/tags/v* (or ~ALL) under GitHub’s fnmatch ref-glob
semantics (an include/exclude of refs/tags/* counts the same as the
literal refs/tags/v*, not just a byte-identical string); that lacks a
creation rule; or whose bypass_actors is explicitly [] (a
structurally-valid ruleset with zero bypass actors would itself block
make release-finalize’s git push origin v$(VERSION), bricking releases —
that is a finding, not just an advisory). A NON-empty bypass_actors list is
not a structural failure (a bypass path is REQUIRED so make release-finalize
can still tag) — instead it prints a CONFIRM before enforcing: line listing
the bypass actors; verify that list is the release identity only (not a
broad Write/Admin role) before flipping the flag. After applying the
ruleset and confirming the bypass list, flip TAG_RULESET_ENFORCED = True
(a one-line change, tested by test_ruleset_drift_review_coverage.py) so
the gap becomes blocking.
Wired into CI (landed alongside the fnmatch/bypass-empty hardening above):
scripts/ruleset_drift_check.py’s tag_creation_findings() calls
tag_protection_findings()/tag_bypass_advisory() against every ruleset
release-canary.yml’s ruleset-drift job already fetches (the same
RULESET_DRIFT_TOKEN-authenticated full-ruleset listing used for the
develop-squash-only/main-release-only checks — no second API call), so
the daily canary run itself surfaces tag-ruleset drift under the same
WARN-until-applied gating as the develop review rule, with no dependency on
release-canary-scheduled-activation beyond the canary running at all.
Live-state note (fill in after applying — do not leave blank):
# Tag-creation ruleset live state
# checked: <YYYY-MM-DD> by: <admin>
# ruleset id: <id> enforcement: <active?> conditions.ref_name.include: <...>
# rules: <creation, ...> bypass_actors: <...>
pypi environment required-reviewers gate (verify/pending admin action)¶
release.yml’s publish job already scopes the real-PyPI publish to the
GitHub environment: pypi (and test-pypi for the test-PyPI path), which
is the correct native mechanism for a required-reviewers/human-approval
gate on publish. The workflow file alone does not prove the pypi
environment has required_reviewers configured on the repo side.
Verify:
gh api repos/joeharris76/BenchBox/environments/pypi
Check the response’s protection_rules array for an entry with
type: "required_reviewers" and a non-empty reviewers list.
Live state at time of writing: not independently re-verified in this
session (no gh/network access available in this sandbox to call the API).
No prior record of required_reviewers being configured for the pypi
environment exists elsewhere in this repo’s admin history
(_project/decisions/), so treat it as unconfirmed/likely absent until an
admin runs the verify command above and records the result here.
Apply (admin only, if the verify command shows no required_reviewers
rule):
gh api -X PUT repos/joeharris76/BenchBox/environments/pypi \
-f 'reviewers[][type]=User' \
-f 'reviewers[][id]=<github-user-id-of-approver>'
Repeat for the test-pypi environment only if the same gate is desired
there (lower risk by design; optional).
After applying (or confirming already applied), record the positive confirmation here — do not leave this section silent about live state.
Release canary and ruleset drift¶
Release readiness has one scheduled/manual canary:
workflow: .github/workflows/release-canary.yml
schedule: daily at 08:00 UTC
freshness_sla: 48h
blocking_suite: (slow or resource_heavy) and not (stress or live_integration)
advisory_suites: stress, live_integration, live cloud credentials, long-running UAT
validate-main-pr.yml keeps the required context name validate-base, but
that job now also runs scripts/release_readiness_check.py for release PRs.
It fails when the latest completed release-canary.yml run is missing, red,
older than 48 hours, or when the tested develop SHA recorded in the canary
summary artifact is not an ancestor of the release PR head. Scheduled canary
runs execute from the default branch, then check out develop before running
release evidence and recording commit_sha in release-canary-summary.json.
For the first release that introduces release-canary.yml, before GitHub can
run the workflow from the default branch, validate-base runs the same
non-fast canary suite and ruleset drift check inline as bootstrap evidence.
Ruleset drift is checked by scripts/ruleset_drift_check.py inside
release-canary.yml. The script parses this runbook for
develop-squash-only and main-release-only, then compares live GitHub
rulesets for required status check contexts, strict-base settings, bypass
actors, linear history, non-fast-forward protection, deletion protection, and
target refs. For develop-squash-only only, it also imports
review_enforcement_findings from
_project/scripts/ruleset_review_enforcement.py (see “Soundness-path review
enforcement” above) and surfaces a missing code-owner-review rule as a
WARNING (non-blocking):-prefixed finding — visible in the job summary and
JSON output, but not yet failing the canary (WARN-until-enforced; flips to
blocking via DEVELOP_REVIEW_RULE_ENFORCED once the admin PUT lands). The
workflow must use the repository secret RULESET_DRIFT_TOKEN
with enough ruleset write/admin visibility for the API to expose
bypass_actors; the default GITHUB_TOKEN is intentionally not used for this
check. If GitHub API access fails, the canary is red; release PRs then fail on
stale/red canary evidence instead of silently trusting comments.
validate-main-pr.yml’s bootstrap invocation (~line 115) runs the identical
script with the identical token, so the same review-rule coverage is reachable
from both CI call sites without a separate code path.
Scheduled activation of release-canary.yml (RESOLVED 2026-07-08)¶
RESOLVED 2026-07-08 by the default-branch switch (Decision A,
branch-default-switch-to-develop). The GitHub default branch is nowdevelop(gh api repos/joeharris76/BenchBox --jq .default_branch→develop). GitHub runson.scheduleworkflows from the default branch’s copy, so every develop-authored scheduled workflow now registers and fires directly — the “land it onmain” problem below no longer exists. Verified 2026-07-08 via the Actions list-workflows API:release-canary.yml(id 309070628),phase3-promotion-review.yml, andorphaned-commit-detector.ymlare now registered (26 workflows, up from 19). Activation options (a)/(b)/(c) and the “Admin steps (w2)” below are superseded and retained only as history. Thenightly.ymlscheduled-workflow-livenessguard now runs fromdevelopdirectly (no “until its file lands onmain” caveat). The canary’s first scheduled run is expected RED on the broken 0.3.0 PyPI release (seerelease-recovery-v0-3-1) — that is the canary working, not a regression.
Historical live state observed 2026-07-05 (release-canary-scheduled-activation TODO, w0):
git ls-tree origin/main --name-only .github/workflows/does not containrelease-canary.yml— the file exists only ondevelop.The Actions list-workflows API returns 19 registered workflows and release-canary is not among them (a workflow absent from the default branch with zero historical runs is never registered), so its
on.schedulecron has never fired and none of its jobs (pypi-latest-installability, ruleset-drift, credential-free-non-fast, plus the release-canary-result aggregator) has ever executed.Same class, second instance:
phase3-promotion-review.yml(quarterly cron0 9 1-7 1,4,7,10 *) is also ondevelopbut absent fromorigin/main, so its schedule has never fired either. The liveness guard below will name it alongside release-canary; the admin should land it onmainin the same pass (or deliberately remove its schedule and record that decision here). Itsreviewjob now checks outdevelopexplicitly via aPHASE3_REVIEW_REFenv var (the sameRELEASE_CANARY_REFshell pattern release-canary.yml uses), so landing the file onmaindoes not leave a scheduled run checking outmain’s stripped tree (no_project/orscripts/phase2_metrics.py) by default.Same class, third instance (#1020 review):
orphaned-commit-detector.yml(weekly cron0 7 * * 1) is also ondevelopbut absent fromorigin/main, so its schedule has never fired either — only its path-filteredpush: branches: [develop]trigger can run, on the cadence of detector/allowlist edits rather than weekly. It already hardcodesref: developon its checkout step (the same shell pattern as release-canary.yml/phase3-promotion-review.yml), so it is safe to land onmainas-is whenever the admin does the next pass for this class of fix — nomain-relative edits needed first.
GitHub runs on.schedule workflows only from the default branch (historically
main; now develop as of 2026-07-08 — see the RESOLVED note above, which
makes the options below historical).
Activation options considered (w1):
(a) Admin lands the current
release-canary.ymlonmainout-of-band (cherry-pick/push through the documented admin flow;mainis push-restricted, so admin-only). Fastest path; content onmainthen goes stale between release-cuts unless (c) also holds.(b) Wait for the next release-cut to carry it. Zero extra action, but the canary stays dead until v0.3.1 ships — the exact window it guards.
(c) Keep
main’s copy a minimal stable scheduled shell that checks outdevelopfor current logic. The file already follows this shape: the test and drift jobs check outdevelopviaRELEASE_CANARY_REF, and pypi-latest-installability needs no checkout at all. Only future workflow-structure edits (job graph, permissions, schedule) need re-landing onmain; test/drift content tracksdevelopautomatically.
Recommendation (recorded 2026-07-05): (a) + (c) together — land the
current develop copy of release-canary.yml on main once (it already is
the (c) shell), keep logic on develop.
Admin steps (w2 — maintainer action, never an agent push):
Land
develop’s.github/workflows/release-canary.ymlonmainvia the admin/release flow.Confirm registration: the workflow appears in
gh api repos/joeharris76/BenchBox/actions/workflows --jq '.workflows[].path'.Trigger
workflow_dispatch(or wait for the next 08:00 UTC cron) and record the run URL here as proof-of-life.Expected first result: RED — pypi-latest-installability correctly fails on the broken 0.3.0 PyPI release (
ModuleNotFoundError: pandason clean install). That is the canary working; cross-link the red run inrelease-recovery-v0-3-1as live pressure for the recovery release. Do not weaken the canary to get a green first run.
Once the canary is live, the scheduled-workflow-liveness job in
nightly.yml (added 2026-07-05, executes once its copy reaches main)
asserts daily that every workflow in the develop tree declaring
on.schedule has a recent run of event=schedule within a
cadence-derived window (3 days for daily crons, up to 100 days for
quarterly ones), so this dead-scheduled-workflow class cannot recur
silently. Deliberate consequence: a scheduled workflow newly authored on
develop reads red in that guard until its file lands on main.
Emergency override is intentionally explicit and SHA-scoped. Admins may set both repository variables below, then remove them after the release:
RELEASE_READINESS_OVERRIDE_SHA: exact release PR head SHA
RELEASE_READINESS_OVERRIDE_REASON: incident or approval record
The override is recorded in the validate-base job summary. Do not use it for
routine canary failures; fix the non-fast canary, ruleset drift, or GitHub API
access and let the canary return to green.
Repository labels¶
The develop-post-merge.yml auto-revert job creates these labels on
demand if they do not exist:
incident:develop-red— used on the auto-revert PR.incident:develop-red-revert-conflict— used on the manual-action issue when the revert path cannot complete (revert conflict, push failure, PR-creation failure).
The on-demand gh label create … || true in the workflow means a fresh
clone or transfer does not need the labels pre-created. They will appear
the first time develop goes red.
Verify:
gh label list --search incident
Re-applying after a transfer or restore¶
If the repo is transferred, restored from backup, or the rules drift, re-apply in this order:
Workflow permissions (
gh api -X PUT … actions/permissions/workflow).Develop ruleset — recreate
develop-squash-onlywith the required contexts list above. The ruleset id will change; update this file and theMakefile/scripts/references that hard-code it.Verify with the
gh api … rulesets/<id> --jq …command above.Push a no-op commit to develop and confirm
develop-post-merge.ymlproduces ametricsartifact and the lint + fast-test jobs are green. This validates that workflow permissions are correct end-to-end.
Out-of-scope¶
This runbook covers only the GitHub admin state that the dev-loop workflows depend on. Other GitHub settings (collaborators, secrets, webhooks, Pages, environments, deploy keys) are out of scope here. Keep them in their own runbook if they grow load-bearing.