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/BenchBox-dev/BenchBox/actions/permissions/workflow
Apply (admin only):
gh api -X PUT repos/BenchBox-dev/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
- Results Explorer browser gate
- ruleset-drift
A code-PR synchronize is not one Develop PR run. The same head SHA also
starts Results Explorer browser tests, PR base guard, auto-merge revocation,
the unconditional develop-refresh-shadow observational workflow, and
ruleset-drift (plus path-filtered siblings such as extension-smoke and
gitignore lint). Documentation (docs.yml) only starts when the diff touches
its own path filter (benchbox/**, docs/**, examples/**, and similar) —
for example a tests/**-only PR does not start it. Split runner minutes from
wall minutes by workflow when judging savings; the next-slowest sibling can
dominate remaining wall time after pr.yml jobs are skipped.
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,
medium-test (added 2026-07-11, #1139 — the medium tier now gates code
PRs pre-merge via the same umbrella, no ruleset change needed),
explorer-tokens, audit-sha, package-smoke, and dependency-audit.
Results Explorer browser gate (added 2026-08-03) is the umbrella job in
.github/workflows/results-explorer-browser.yml. It is required because the
Chromium full-suite job’s own name has claimed to block since it was written,
while the ruleset required only ci-required-result — so the full e2e/ suite
gated nothing. The gate job, not the Chromium job, holds the required context:
the browser jobs are conditional, and GitHub keeps a PR unmergeable forever
waiting on a required check that never reports. The gate runs if: always(),
passes when Chromium succeeded or when no explorer-relevant path changed, and
fails closed if change detection itself broke. Firefox and WebKit stay advisory
and are deliberately absent from its needs.
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: true
required_linear_history: true
non_fast_forward: true
required_pull_request_reviews: squash-only PRs
deletion: blocked
bypass_actors: (none)
Current-base checks are required because instruction budgets and other
repository-wide invariants are not additive per PR. Without the strict policy,
two PRs can each pass against the same older base and exceed an invariant when
merged in sequence. The tradeoff is deliberate: when develop advances, an
otherwise-green PR must refresh its required checks before it can merge.
refresh-shadow (added with the strict-base refresh shadow rollout) is the
observational job in .github/workflows/develop-refresh-shadow.yml. It is
not a required context. It classifies exact develop refreshes using the
trusted base copy of scripts/pr_refresh_certification.py and publishes a
bounded artifact. It cannot skip Develop PR lanes, cannot satisfy
ci-required-result, and does not change auto-merge or ruleset 15611785.
.github/workflows/pr.yml also uploads pr-certification-identity and
pr-certification-lanes artifacts so a later activation gate can bind a full
run to a specific head, base, merge tree, workflow fingerprint, and lane
set. Missing artifacts fail closed to full_required.
Verify:
gh api repos/BenchBox-dev/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"),
merge_queue: any(.rules[]; .type == "merge_queue")
}'
Native Merge Queue Configuration (Post-v0.4.0)¶
When Native Merge Queue is activated on develop-squash-only (ruleset id 15611785), the following rule parameters govern queue operations:
{
"type": "merge_queue",
"parameters": {
"check_response_timeout_minutes": 60,
"grouping_strategy": "ALLGREEN",
"max_entries_to_build": 5,
"max_entries_to_merge": 1,
"merge_method": "SQUASH",
"min_entries_to_merge": 1,
"min_entries_to_merge_wait_minutes": 0
}
}
Speculative Integration:
max_entries_to_build: 5evaluates up to 5 concurrent pull requests speculatively without serializing check waits.Atomic Squash:
merge_method: SQUASHpreserves the single-commit linear history invariant.Soundness Gate: Soundness PRs are withheld from auto-enqueue by
auto_merge_soundness_paths.pyand require CODEOWNERS approval before entry.Rollback: Disable the
merge_queuerule object in ruleset15611785to immediately revert to standard squash merges.
Soundness-path review enforcement (enforced; operational caution)¶
Live verification on 2026-07-21 shows that develop-squash-only (ruleset id
15611785) is active and its pull_request rule has
require_code_owner_review: true, with required_approving_review_count: 0
and no bypass actors. This current live state supersedes the 2026-07-18
retirement note, which was based on the rule not yet being applied.
The soundness gate, as operated:
SOUNDNESS_PREFIXESin_project/scripts/auto_merge_soundness_paths.py(mirrored 1:1 into.github/CODEOWNERS, lockstep pinned bytests/unit/test_auto_merge_soundness_paths.py) classifies the soundness-critical surface: 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, thebenchbox/sql_compat/rule-dispatch core), and the gate machinery itself (the predicate,.github/workflows/auto-merge-on-open.yml, and the PyPI-publishing.github/workflows/release.yml).make pr-openno longer arms auto-merge at all;make pr-ready(ormake pr-open READY=1) does, so a PR cannot merge while a follow-up commit is still being written. Arming at creation stranded three commits in one session, two of them the fixes for their own review findings.make pr-openalso refuses whenorigin/developis not an ancestor ofHEAD(open-stale). Absorb current develop withmake pr-refresh(one PR at a time).STALE=1is the explicit escape.pr-openmust not mergedevelopitself; that would turnpr-fanoutinto a refresh storm. See_project/decisions/behind-pr-occurrence-2026-08-16.md..github/workflows/auto-merge-on-open.ymlis revoke-only: it never arms on any event (baregh pr createdoes not auto-arm, and the historicalready_for_reviewarm point — which never fired once, drafts being unused — was deleted per_project/decisions/auto-merge-policy-consolidation-2026-08-06.md, D2).opened/reopened/synchronize/labeledre-evaluate revocation only (soundness paths or the hold label). The soundness check unions the base-ref predicate with the PR checkout copy so a gate widened mid-flight still revokes and a PR cannot weaken its own gate.Durable holds every layer honours: draft (job/sweep skip) and label
no-auto-merge(make pr-arm-auto-merge/pr-readyrefuse to arm; workflow disables; nightly green-unmerged sweep never enables auto-merge and does not classify the label as stranded). Seedocs/operations/pr-triage.md“Durable auto-merge holds”.The Makefile arming path WITHHOLDS squash auto-merge for PRs touching those paths, and
auto-merge-on-open.ymlrevokes it on a later push that newly touches them. CI cannot catch a change that redefines the oracle it validates against, so these PRs must not merge hands-free.The active ruleset now supplies the repo-layer control; the owner must still review and merge manually because the current single-owner account cannot approve its own PR. Adding a second code-owner or changing the PR identity model would remove that operational deadlock; alternatively, an admin must remove the live rule before it blocks a soundness-path release.
scripts/ruleset_drift_check.py now imports the shared
review_enforcement_findings predicate and treats a missing or false
require_code_owner_review as a blocking finding through
DEVELOP_REVIEW_RULE_ENFORCED = True. The daily release-canary.yml run and
validate-release-pr.yml bootstrap use the same path. The standalone check is
available for an immediate live verification:
gh api repos/BenchBox-dev/BenchBox/rules/branches/develop \
| uv run --project _project/scripts -- python _project/scripts/ruleset_review_enforcement.py --rules-file -
The predicate deliberately does not assert required_approving_review_count:
that setting is branch-wide and would gate every develop PR. The checker
reports drift; it does not make the current single-owner self-approval rule
operable. Treat a green drift check as evidence of configuration, not proof
that a soundness-path PR is mergeable under the current identity model.
The review predicate and its blocking/default-plus-explicit-override behavior
are covered by tests/unit/release/test_ruleset_review_enforcement.py and
tests/unit/release/test_ruleset_drift_review_coverage.py.
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 — release¶
Ruleset name: release-only, targets refs/heads/release.
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-release-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/BenchBox-dev/BenchBox/rulesets --jq '.[] | {id, name, target}'
gh api repos/BenchBox-dev/BenchBox/rulesets/<release-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-release job that asserts the triggering ref (tag push
or workflow_dispatch) is release 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) — only an
admin PAT can.
Tag creation restricted to release flow (enforced)¶
Current control: 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-release stops a stray tag from
reaching build/publish only if the tagged commit isn’t on release — it does
nothing to stop someone with push access from creating a v* tag on a
release commit out of band (e.g. re-tagging an old release 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/BenchBox-dev/BenchBox/rulesets --jq '.[] | {id, name, target, conditions}'
Live verification on 2026-07-21 shows v-tag-restricted (ruleset id
18774756) is active, targets refs/tags/v*, carries a creation rule,
and has the release-finalize bypass actor User:57046 with bypass_mode: always. The bypass is required for make release-finalize to push its tag;
confirm it remains scoped to that identity.
The ruleset was applied by an admin on 2026-07-10. No PR-side admin mutation is required; the commands below remain as historical context for the original application:
gh api -X POST repos/BenchBox-dev/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 carries a
tag_protection_findings() predicate and an enforced TAG_RULESET_ENFORCED
flag. 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/BenchBox-dev/BenchBox/rulesets --jq '.[].id')
for id in $ids; do gh api repos/BenchBox-dev/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). An explicit enforce_tag_rule=False call remains available
for migration fixtures, but the live default is 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/release-only checks — no second API call), so
the daily canary run itself surfaces tag-ruleset drift as a blocking finding,
with no dependency on
release-canary-scheduled-activation beyond the canary running at all.
Live-state note:
# Tag-creation ruleset live state
# checked: 2026-07-21 by: joeharris76 (admin)
# ruleset id: 18774756 enforcement: active conditions.ref_name.include: [refs/tags/v*]
# rules: [creation] bypass_actors: [User:57046 (always)]
Applied 2026-07-10: v-tag-restricted (id 18774756) is live and active. The
bypass list is a single User actor (57046, the release-finalize identity) —
confirmed to be the release identity only, not a broad Write/Admin role, which
is what release-finalize’s git push origin v$(VERSION) needs to still
succeed. With this confirmed, TAG_RULESET_ENFORCED in
_project/scripts/ruleset_review_enforcement.py is flipped to True, so a
future regression (ruleset deleted, made inactive, ref narrowed, creation rule
dropped, or bypass emptied) becomes a blocking drift finding instead of a
warning.
pypi environment required-reviewers gate (configured; observed 2026-08-10)¶
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 — only a
live environments API read does. GitHub enforces the gate at deployment time
when it is configured. scripts/ruleset_drift_check.py reads that API in the
existing release-canary drift job and fails closed when the environment,
required-reviewers rule, reviewer identity, admin-bypass posture, or
self-review posture differs from the pin below. The check reuses
RULESET_DRIFT_TOKEN, which therefore needs environment-read visibility as
well as full ruleset visibility.
Verify:
gh api repos/BenchBox-dev/BenchBox/environments/pypi \
--jq '{name, can_admins_bypass, protection_rules: [.protection_rules[] | {type, prevent_self_review, reviewers: [.reviewers[]?.reviewer.login]}]}'
Live verification on 2026-08-10 (command above) shows the pypi environment
already carries a required-reviewers gate:
# pypi environment live state
# checked: 2026-08-10 by: joeharris76 (admin)
# command: gh api repos/BenchBox-dev/BenchBox/environments/pypi --jq '{name, can_admins_bypass, protection_rules: [.protection_rules[] | {type, prevent_self_review, reviewers: [.reviewers[]?.reviewer.login]}]}'
# observed: {"name":"pypi","can_admins_bypass":true,"protection_rules":[{"reviewers":["joeharris76"],"type":"required_reviewers","prevent_self_review":false}]}
# type: required_reviewers reviewer login(s): joeharris76 (User id 57046)
# can_admins_bypass: true prevent_self_review: false wait_timer: null deployment_branch_policy: null
No admin mutation required for this gate — it is already configured. The
can_admins_bypass: true + prevent_self_review: false pair is the accepted
single-admin / self-review posture for this repo (one maintainer is both the
required reviewer and an admin who can still approve or bypass their own
deployment). The release canary continuously detects a removed rule, emptied
or replaced reviewer set, deleted environment, or changed bypass/self-review
flag. Re-run the verify command above for operator diagnosis when the canary
reports drift.
test-pypi intentionally has no protection rules (lower friction for dry-run
publish paths). Observed on 2026-08-05:
gh api repos/BenchBox-dev/BenchBox/environments/test-pypi --jq '{name, protection_rules}'
# {"name":"test-pypi","protection_rules":[]}
That empty gate is accepted by design; do not copy the real-PyPI
required_reviewers rule onto test-pypi unless a future policy change
explicitly wants the same friction on the test path.
Historical re-apply reference (admin only, if the live verify ever shows the gate missing):
gh api -X PUT repos/BenchBox-dev/BenchBox/environments/pypi \
-f 'reviewers[][type]=User' \
-f 'reviewers[][id]=57046'
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 is an advisory campaign. Release readiness requires the
blocking release canary; see docs/operations/release-guide.md “UAT matrix
campaign evidence (advisory)” for the optional UAT report.
validate-release-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 for every develop PR by
develop-ruleset-drift.yml and independently by release-canary.yml. The PR
workflow uses pull_request_target, checks out only the trusted base SHA, and
never executes pull-request-head code with the admin-visible token. Its
ruleset-drift job is a required develop-squash-only context, so drift blocks
the next merge instead of waiting for the scheduled canary. The script parses this runbook for
develop-squash-only and 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, it also applies the shared
review_enforcement_findings predicate and treats a missing or false
require_code_owner_review as a blocking finding through
DEVELOP_REVIEW_RULE_ENFORCED = True. The
Both workflows 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-release-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/BenchBox-dev/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/BenchBox-dev/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¶
The 2026-08-21 cutover to org BenchBox-dev is documented in
docs/operations/github-org-transfer.md (gates G0–G7b, Pages
serving-only vs publish, exclusive Pages domain lock: do not click
org Verify before G4, org protected_domain verification after G4,
environment deployment-branch policies, RULESET_DRIFT_TOKEN
authentication, and never recreate joeharris76/BenchBox). Follow
that runbook for an ownership transfer. The numbered restore below is
the subset that still applies after a backup restore or ruleset wipe
without changing owners.
If the repo is restored from backup or the rules drift without a transfer, 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. Prefer resolving the live ruleset by name (develop-squash-only) in workflows; do not treat a stale numeric id as authority.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.