GitHub Policy
One line: on production repos the merge gate is the machine, not a second human. Required status checks + Copilot review + secret scanning are enforced; human approval is not required, because measurement showed it never actually happened and pretending otherwise trained everyone to click through every gate.
Why this policy looks unusual
Most orgs require a human approval to merge. PSI deliberately does not, on the evidence.
Measured across the 11 lifecycle=production repos (883 merged PRs, August 2026):
| Metric | Result |
|---|---|
| Merged with a second human’s approval | 31 (3.5%) |
| Self-merged by the author | 781 (88.4%) |
| Copilot reviewed | 562 (63.6%) |
| Ruleset evaluations that were a bypass | 9 of 9 (100%) |
And of those 31 approvals, only two were a human reviewing the primary author’s code. Eight were approvals of Dependabot version bumps. The one review relationship that genuinely functioned was one senior engineer reviewing one other engineer — and that engineer has since moved off these repos.
The cause is structural, not cultural: PSI has ~6 engineers and one person authors ~61% of all PRs. There is no second qualified reviewer available for most changes. A required approval in that shape is not a control — it is a permanent false negative, satisfied only by override.
The real cost was not the missing review. It was that overriding became the normal way to merge (enforce_admins = false everywhere, plus an unconditional bypass for the whole business-systems team). Once the routine path runs through the override, people stop reading gates at all — including the ones that would have caught something.
So the policy stops requiring what does not happen, and puts real weight behind what a machine can actually enforce.
The tier model
Enforcement is keyed to the required org custom properties, not to hand-maintained repo lists:
| Property | Values |
|---|---|
lifecycle | production, pilot, internal-tool, archive |
domain | business-systems, controls, infra, wiki, experiment |
owner-team | free text (team slug) |
Only lifecycle=production carries enforced rules. Everything else is advisory. This is why the properties are required = true on repo creation — a repo with no lifecycle would fall outside governance silently.
What gates a merge on a production repo
One system: rulesets. Classic branch protection is retired on production repos. Rulesets record every bypass in the rule-suites API, which is the only way to know whether override is rare or routine — classic protection’s enforce_admins = false recorded nothing.
Org ruleset — Production: machine-gated merge (no human approval required), scoped to lifecycle=production, default branch:
required_approving_review_count = 0— human approval not requiredrequire_code_owner_review = falserequired_review_thread_resolution = true— unresolved conversations still blockallowed_merge_methods = [squash]copilot_code_reviewwithreview_on_push = true- no force-push, no branch deletion
- bypass: OrganizationAdmin only (the blanket
business-systemsteam bypass is removed)
Per-repo ruleset — Required checks, one per production repo, carrying that repo’s real check names with strict_required_status_checks_policy = true. This is the actual gate.
Org-wide security: secret scanning, push protection, and Dependabot alerts on — for new repos by default and enabled on existing ones. With no second reviewer, this is the one class of defect a machine covers completely.
Choosing what to require
A required check that does not report on some PRs blocks those PRs forever. So:
Only require a check name observed on every sampled PR head.
A path-filtered or matrix-named job fails this test — Build: ${{ matrix.folder }} never resolves, and a docs-only PR legitimately produces no build checks at all. For those repos, require a CI Gate instead: one aggregator job that always runs, waits for whatever checks the PR did produce, and reports a single verdict. Reference implementation: .github/workflows/ci-gate.yml in PSI.All.
Run it on the Linux runners, not the Windows build runner — if that runner is offline, a gate that never reports is indistinguishable from success.
Current state
| Repo | Required checks | Status |
|---|---|---|
| PRGJSMES | SQL Lint (block destructive DDL), Permission Sync Check, Build API, Build Frontend, Backend Tests, E2E Tests (Playwright), CRA Leftovers Check | Gated |
| psi-portal | verify | Gated |
| PSI.UniData.API | build | Gated |
| psi-notify-bot | ci | Gated |
| PSI.All | CI Gate | Pending — lands with PSI.All PR #242 |
| PSI-Wiki-Site | — | build-and-test does not run on every PR; needs a CI Gate |
| redbook-web | — | test does not run on every PR; needs a CI Gate |
| bom-explorer-web | — | No PR CI at all |
| psi-data-pipeline | — | No PR CI at all |
| psi-winget-source | — | No PR CI at all |
| PSI.BC.ALL | — | No PR CI at all |
Be honest about the bottom seven rows: for those repos “the machine is the gate” is not yet true. Their only real controls today are Copilot review and secret scanning. Adding a CI Gate to each is the outstanding work, and it is the highest-value engineering task on this policy.
Break-glass, and how it is measured
Bypass is possible but visible — deliberately not impossible.
A gate that truly cannot be bypassed stops all work the first time a check goes flaky, and the workaround people reach for is disabling the ruleset outright, which is invisible. So break-glass is OrganizationAdmin, and every use is recorded in rule-suites.
The KPI is the bypass rate, not the review rate:
cd C:\GIT\psi-azure-admin
.\scripts\Invoke-GitHubPolicyBaseline.ps1 -ReportBaseline at adoption: 100% bypass (9 of 9). Target: trending to near zero.
If the bypass rate stays high, the required checks are wrong or missing — not the people. Fix the checks.
CODEOWNERS
CODEOWNERS is routing, not a gate. require_code_owner_review is false, because with a single owner listed the sole owner cannot approve their own PR and every one of their PRs deadlocks.
Keep CODEOWNERS for auto-requesting the right person on high-risk paths (SQL, EF models, migrations, permission seeds, deploy workflows, app config). Do not treat its presence as evidence that those paths were reviewed.
Applying the policy
The policy is code. Do not click it into the UI — the settings drift and nobody can tell what changed.
cd C:\GIT\psi-azure-admin
# Audit (read-only) - prints every change it would make
.\scripts\Invoke-GitHubPolicyBaseline.ps1
# Apply
.\scripts\Invoke-GitHubPolicyBaseline.ps1 -Apply
# Scope to one repo
.\scripts\Invoke-GitHubPolicyBaseline.ps1 -Repo PRGJSMES -ApplyThe script is idempotent and re-runnable; run the audit whenever you suspect drift.
New production repo checklist
- Set
domain,lifecycle,owner-team(required at creation —lifecycle=productionis what pulls the repo into enforcement). - Add PR CI that produces at least one check name that runs on every PR. If jobs are path-filtered or matrix-named, add a CI Gate.
- Add the repo to
$script:BaselineinInvoke-GitHubPolicyBaseline.ps1with its real check names. - Run the script with
-Apply, then confirm with-Report. - Add
.github/dependabot.ymlper the Dependency Update Policy.
Related
- Dependency Update Policy — Dependabot config, security vs. version updates
- Schema Change Policy — what the SQL lint gate is protecting
- Web App Compliance Standard — Azure-side controls
- Deploy to Azure — what happens after merge