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):

MetricResult
Merged with a second human’s approval31 (3.5%)
Self-merged by the author781 (88.4%)
Copilot reviewed562 (63.6%)
Ruleset evaluations that were a bypass9 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:

PropertyValues
lifecycleproduction, pilot, internal-tool, archive
domainbusiness-systems, controls, infra, wiki, experiment
owner-teamfree 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 rulesetProduction: machine-gated merge (no human approval required), scoped to lifecycle=production, default branch:

  • required_approving_review_count = 0 — human approval not required
  • require_code_owner_review = false
  • required_review_thread_resolution = true — unresolved conversations still block
  • allowed_merge_methods = [squash]
  • copilot_code_review with review_on_push = true
  • no force-push, no branch deletion
  • bypass: OrganizationAdmin only (the blanket business-systems team bypass is removed)

Per-repo rulesetRequired 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

RepoRequired checksStatus
PRGJSMESSQL Lint (block destructive DDL), Permission Sync Check, Build API, Build Frontend, Backend Tests, E2E Tests (Playwright), CRA Leftovers CheckGated
psi-portalverifyGated
PSI.UniData.APIbuildGated
psi-notify-botciGated
PSI.AllCI GatePending — lands with PSI.All PR #242
PSI-Wiki-Sitebuild-and-test does not run on every PR; needs a CI Gate
redbook-webtest does not run on every PR; needs a CI Gate
bom-explorer-webNo PR CI at all
psi-data-pipelineNo PR CI at all
psi-winget-sourceNo PR CI at all
PSI.BC.ALLNo 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 -Report

Baseline 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 -Apply

The script is idempotent and re-runnable; run the audit whenever you suspect drift.

New production repo checklist

  1. Set domain, lifecycle, owner-team (required at creation — lifecycle=production is what pulls the repo into enforcement).
  2. 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.
  3. Add the repo to $script:Baseline in Invoke-GitHubPolicyBaseline.ps1 with its real check names.
  4. Run the script with -Apply, then confirm with -Report.
  5. Add .github/dependabot.yml per the Dependency Update Policy.