Skip to content

docs: document fleet merge-gate audit rules - #77

Merged
Freudator86 merged 2 commits into
mainfrom
fm/hlr-fleet-ci-gate-mechanism-consistency
Aug 6, 2026
Merged

docs: document fleet merge-gate audit rules#77
Freudator86 merged 2 commits into
mainfrom
fm/hlr-fleet-ci-gate-mechanism-consistency

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Settle one merge-gate mechanism across the heavyliftrental (HLR) fleet and record where the fleet's gates live so a future audit queries the right endpoint. Investigation established that a GitHub required-check merge gate can live in two independent mechanisms: repository rulesets (/repos//rulesets) and classic branch protection (/repos//branches/main/protection); querying one returns an answer that reads identically to 'no gate' for a repo gated by the other. The fleet standard was chosen as repository rulesets with no bypass actors (12 of 13 repos were already there, it is the newer mechanism, it matches the existing admiralty doctrine, and it closes the admin walk-through classic protection leaves open). The one odd repo, hlr-research, was on the classic path only as a 2026-08-04 rollout artifact (a ruleset could not activate until its workflow ran on main), not by any intrinsic need, so it was converted to a no-bypass ruleset and its gate re-proven live (broke a real WLL-classification invariant -> required check red + mergeable_state blocked; restored -> green + clean; throwaway PR closed unmerged, branch deleted, main untouched).

IMPORTANT for the reviewer reading only the diff: the actual mechanism conversion happened on GitHub infrastructure (repository settings via the API), which is not version-controlled, so the ONLY committed change is a single new documentation file, docs/merge-gate-audit.md. That is by design and matches the task: the deliverable to this repo is the documentation. The doc records the two-mechanism reality, the rule that an audit MUST query BOTH endpoints (push restrictions remain classic-only on 10 of 13 repos, a real coexistence reason), how to read GitHub Actions results via /commits//check-runs rather than the Statuses API, the proof method, and the bypass picture for both mechanisms including a captain-facing finding that the 10 classic push restrictions have enforce_admins:false (flagged as a separate decision, not changed here). This is firstmate's own shared tracked material; the doc follows repo style (one sentence per line, plain dashes, no em dashes) and is intentionally a reference doc under docs/ rather than inline in AGENTS.md.

What Changed

  • Added docs/merge-gate-audit.md documenting that GitHub merge-gate audits must query both repository rulesets and classic branch protection, read Actions results through check-runs, and prove required-check gates by observing blocked and clean PR states.
  • Recorded the current heavyliftrental fleet gate map, including the ruleset standard, hlr-research conversion details, bypass posture, and the remaining separate enforce_admins decision for classic push restrictions.
  • Linked the new audit reference from README.md and the admiralty fleet repo documentation.

Risk Assessment

✅ Low: Captain, the change is a single bounded documentation addition, and I did not find any material correctness, safety, or maintainability risks in the new merge-gate audit guidance.

Testing

Diff whitespace validation passed; the repository's changed-file selector intentionally selected no suites for this generic docs-only path; a focused content checklist confirmed the required audit method and fleet-state claims; the doc rendered to HTML for reviewer-visible inspection; the no-em-dash style check passed; and the tracked working tree remained clean.

Evidence: Rendered merge-gate audit documentation
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Rendered merge-gate-audit.md</title><style>
body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#f6f7f9;color:#1b1f24;line-height:1.55}.page{max-width:980px;margin:0 auto;padding:48px 28px 72px;background:white;box-shadow:0 0 0 1px #d8dee4;min-height:100vh}h1{font-size:34px;line-height:1.16;margin:0 0 24px;color:#0f172a}h2{font-size:23px;margin:34px 0 12px;border-top:1px solid #d8dee4;padding-top:24px}h3{font-size:18px;margin:28px 0 10px}p{margin:10px 0}ul,ol{padding-left:26px;margin:10px 0}li{margin:7px 0}code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#eef2f7;padding:1px 5px;border-radius:4px;font-size:.94em}table{border-collapse:collapse;width:100%;margin:16px 0;table-layout:fixed}th,td{border:1px solid #d0d7de;padding:9px 10px;text-align:left;vertical-align:top;word-wrap:break-word}th{background:#f1f5f9}.meta{font-size:13px;color:#57606a;margin-bottom:20px}
</style></head><body><main class="page"><div class="meta">Rendered from docs/merge-gate-audit.md for no-mistakes evidence on 2026-08-05T12:40:16.728Z</div><h1>Auditing merge gates: query both mechanisms, then prove the gate</h1>

<p>A GitHub required-status-check gate that blocks merges can live in either of two independent mechanisms.</p>
<p>An audit that queries one mechanism gets a confidently wrong answer about the other, and that wrong answer looks exactly like a clean result.</p>
<p>This doc records where a fleet's gates live, which endpoints an audit must query, and how to prove a gate actually blocks.</p>

<h2>The two mechanisms</h2>

<ul>
<li><strong>Repository rulesets</strong> at <code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/rulesets</code> (then <code>GET .../rulesets/&lt;id&gt;</code> for the rules).</li>
</ul>
<p>  A ruleset gate is a <code>required_status_checks</code> rule naming one or more contexts.</p>
<ul>
<li><strong>Classic branch protection</strong> at <code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/branches/&lt;branch&gt;/protection</code>.</li>
</ul>
<p>  A classic gate is the <code>required_status_checks</code> block naming contexts.</p>

<p>Both genuinely block merges.</p>
<p>Neither is a defect.</p>
<p>GitHub also keeps a separate control, the push restriction (<code>restrictions</code>), only in classic branch protection, with no ruleset equivalent used in this fleet, so classic protection can be present purely to restrict who may push to the branch, carrying no required check at all.</p>

<h2>The both-endpoints rule</h2>

<p>An audit MUST query both endpoints and combine the answers.</p>
<p>The reason is a measurement trap, not a preference.</p>

<ul>
<li>Querying <code>/rulesets</code> on a repo gated by classic branch protection returns <code>[]</code>, which reads exactly like "no gate" on a repo whose gate is active and proven blocking.</li>
<li>Querying <code>/branches/main/protection</code> on a repo gated by a ruleset returns <code>Branch not protected (HTTP 404)</code>, which also reads like "no gate".</li>
</ul>

<p>Treat an empty <code>/rulesets</code> array as "check classic protection", never as "unprotected".</p>
<p>Treat a <code>404</code> from <code>/branches/main/protection</code> as "check rulesets", never as "unprotected".</p>

<p>A ruleset being <code>enforcement: active</code> is not by itself a gate.</p>
<p>Confirm the ruleset actually contains a <code>required_status_checks</code> rule naming a context.</p>
<p>An active ruleset with no such rule requires nothing.</p>

<h2>Reading the check result</h2>

<p>The GitHub Statuses API is the wrong endpoint for GitHub Actions results.</p>
<p><code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/commits/&lt;ref&gt;/status</code> returns <code>total_count: 0</code> for Actions checks, so an audit reading Statuses concludes "no checks ran" for a repo whose checks all ran.</p>
<p>Actions checks report through <code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/commits/&lt;ref&gt;/check-runs</code>, which is where <code>conclusion: failure</code> lives.</p>

<p>When reading a pull request's own state, note that <code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/pulls/&lt;n&gt;</code> can return a stale <code>head.sha</code> and stale check state for a short window after a push.</p>
<p>Poll <code>check-runs</code> against the head SHA you pushed, read from <code>git rev-parse HEAD</code> or <code>git ls-remote</code>, rather than trusting the pull request object's cached <code>head.sha</code>.</p>

<h2>The bypass dimension</h2>

<p>A gate that the person most likely to be in a hurry can step around is worth knowing about explicitly.</p>
<p>Query the bypass posture of every gate, on whichever mechanism holds it.</p>

<ul>
<li>Ruleset: read <code>bypass_actors</code> and <code>current_user_can_bypass</code> on the ruleset.</li>
</ul>
<p>  <code>bypass_actors: []</code> with <code>current_user_can_bypass: never</code> means no one, including an administrator, can merge past the required check.</p>
<ul>
<li>Classic branch protection: read <code>enforce_admins</code>.</li>
</ul>
<p>  <code>enforce_admins.enabled: false</code> means an administrator can step around every protection on the branch, including the required check and the push restriction.</p>

<p>The two mechanisms differ here in a way that matters.</p>
<p>A ruleset with no bypass actors cannot be walked through by an administrator.</p>
<p>Classic branch protection with <code>enforce_admins: false</code> can.</p>
<p>So moving a required check from classic protection into a no-bypass ruleset closes the administrator bypass on that check as a side effect, and the change should be disclosed as such rather than treated as invisible.</p>

<h2>Proving a gate</h2>

<p>A gate is not proven by reading its configuration.</p>
<p>A configuration that reads correct is exactly the evidence class that hides a gate which no longer blocks.</p>
<p>Prove a required-check gate by breaking a real invariant and watching the gate refuse the merge, then restoring it and watching the gate clear.</p>

<ol>
<li>On a throwaway branch, change a source file so a genuine regression test fails, not a fabricated <code>assert False</code>.</li>
<li>Open a pull request and poll <code>check-runs</code> on its head until the required check reaches <code>conclusion: failure</code>.</li>
<li>Confirm the merge is refused by reading <code>mergeable_state: blocked</code> on the pull request.</li>
</ol>
<p>   Do not call the merge endpoint to "test" this; a blocked <code>mergeable_state</code> with a red required check is the refusal, and calling merge risks landing it.</p>
<ol>
<li>Restore the invariant, push, and poll <code>check-runs</code> on the new head until <code>conclusion: success</code> and <code>mergeable_state: clean</code>.</li>
<li>Close the pull request without merging, then delete its remote branch explicitly.</li>
</ol>
<p>   <code>gh pr close --delete-branch</code> (and the <code>gh-axi</code> wrapper) accepts the flag but does not delete the remote branch, so run <code>git push origin --delete &lt;branch&gt;</code> and confirm <code>GET /repos/&lt;owner&gt;/&lt;repo&gt;/branches/&lt;branch&gt;</code> returns <code>404</code>.</p>

<h2>Current heavyliftrental fleet state (2026-08-05)</h2>

<p>Thirteen fleet repositories carry a required-check merge gate: <code>hlr-certsync</code>, <code>hlr-vat-steward</code>, <code>hlr-adsbot</code>, <code>hlr-einkauf</code>, <code>hlr-engineering-vault</code>, <code>hlr-knowledge</code>, <code>hlr-infra</code>, <code>hlr-librechat</code>, <code>hlr-tank-cad</code>, <code>hlr-reporting</code>, <code>hlr-dms</code>, <code>hlr-pim</code>, and <code>hlr-research</code>.</p>

<p>The chosen fleet standard for the required-check gate is the repository ruleset with no bypass actors.</p>
<p>The reasons are that twelve of the thirteen were already there, that rulesets are the newer mechanism, that the admiralty branch-protection doctrine already prescribes a ruleset with no bypass actors (see <code>admiralty-fleet-repo.md</code>), and that a no-bypass ruleset closes the administrator walk-through that classic protection leaves open.</p>

<p><code>hlr-research</code> was the one repository on the classic path.</p>
<p>The reason was a rollout artifact, not a property of the repository: during the 2026-08-04 rollout a ruleset could not be activated until its workflow had already run on <code>main</code> and produced its check, or the repository would lock (the <code>hlr-reporting</code> bootstrap deadlock), and <code>hlr-research</code> correctly refused to activate until its <code>pr-tests.yml</code> existed on <code>main</code>.</p>
<p>That workflow now exists on <code>main</code> and produces the <code>Research regression tests</code> check on every pull request, so the blocker is gone and the classic path was no longer required.</p>

<p>On 2026-08-05 <code>hlr-research</code> was converted:</p>
<p>its required-check gate now lives in ruleset <code>Research required PR tests</code> (id <code>20456354</code>), with <code>strict_required_status_checks_policy: true</code>, context <code>Research regression tests</code>, <code>bypass_actors: []</code>, <code>current_user_can_bypass: never</code>, matching the other twelve.</p>
<p>Its classic branch protection was reduced to the push restriction only (<code>restrictions.users: [Freudator86]</code>, no required check), matching the ten peers that keep a push restriction.</p>
<p>The gate was re-proven after conversion: a broken WLL-classification invariant drove <code>Research regression tests</code> to <code>conclusion: failure</code> and the pull request to <code>mergeable_state: blocked</code>; restoring the invariant drove the check to <code>conclusion: success</code> and the pull request to <code>mergeable_state: clean</code>; the throwaway pull request was closed unmerged and its branch deleted and confirmed <code>404</code>.</p>

<p>After this change, the required-check gate is uniformly on rulesets for all thirteen, so a required-check-only audit has a single home today.</p>
<p>The both-endpoints rule still stands, for two durable reasons:</p>
<p>a push restriction lives only in classic branch protection and exists on ten of the thirteen repositories (<code>hlr-certsync</code>, <code>hlr-engineering-vault</code>, and <code>hlr-knowledge</code> have no classic protection at all), and a repository added before its ruleset is safe to activate will sit on classic protection exactly as <code>hlr-research</code> and <code>hlr-reporting</code> did during rollout.</p>

<h3>Where each control lives, and which endpoint answers for it</h3>

<table>
<tr><th>Control</th><th>Mechanism</th><th>Endpoint to query</th></tr>
<tr><td>Required-check merge gate (all 13)</td><td>Repository ruleset</td><td><code>/repos/&lt;r&gt;/rulesets</code> then <code>/rulesets/&lt;id&gt;</code></td></tr>
<tr><td>Push restriction (10 of 13)</td><td>Classic branch protection</td><td><code>/repos/&lt;r&gt;/branches/main/protection</code></td></tr>
<tr><td>Actions check result on a commit</td><td>Checks API</td><td><code>/repos/&lt;r&gt;/commits/&lt;ref&gt;/check-runs</code></td></tr>
</table>

<h3>Bypass picture</h3>

<ul>
<li>All thirteen required-check ruleset gates: <code>bypass_actors: []</code>, <code>current_user_can_bypass: never</code>.</li>
</ul>
<p>  No one, including the administrator, can merge past a red required check.</p>
<ul>
<li>The ten classic push restrictions: <code>enforce_admins.enabled: false</code>.</li>
</ul>
<p>  An administrator can push directly to <code>main</code>, stepping around the push restriction.</p>
<p>  In this single-maintainer fleet the sole administrator (<code>Freudator86</code>) is also the sole user the push restriction allows, so the bypass grants that user nothing extra today; it would matter if another administrator were added.</p>
<ul>
<li>Before conversion, <code>hlr-research</code>'s required check sat in classic protection with <code>enforce_admins: false</code>, so it was administrator-bypassable, unlike the other twelve.</li>
</ul>
<p>  The conversion to a no-bypass ruleset closed that one hole and is the only bypass state this task changed.</p>

<p>Whether to set <code>enforce_admins: true</code> on the ten push restrictions is a separate decision, deliberately not taken here, and is recorded so it can be decided rather than discovered later.</p>

<h2>Maintaining this file</h2>

<p>Keep this file to the durable method (query both mechanisms, read <code>check-runs</code>, prove the gate) plus a dated snapshot of the fleet's current gate map.</p>
<p>When the fleet's gate configuration changes, update the dated snapshot rather than appending a second one, and re-run the both-endpoints survey before trusting any claim that a repository is or is not gated.</p>

</main></body></html>
Evidence: Audit procedure content checklist

merge-gate-audit documentation content check source: docs/merge-gate-audit.md PASS both_endpoints: An audit MUST query both endpoints and combine the answers. PASS rulesets_endpoint: Repository rulesets PASS branch_protection_endpoint: Classic branch protection PASS statuses_wrong: The GitHub Statuses API is the wrong endpoint for GitHub Actions results. PASS check_runs: /commits/<ref>/check-runs PASS proof_failure: mergeable_state: blocked PASS proof_success: mergeable_state: clean PASS fleet_standard: The chosen fleet standard for the required-check gate is the repository ruleset with no bypass actors. PASS hlr_research_conversion: hlr-researchwas converted&#10;PASS no_bypass: bypass_actors: []&#10;PASS push_restrictions: Push restriction (10 of 13)&#10;PASS enforce_admins_decision: Whether to setenforce_admins: true` on the ten push restrictions is a separate decision result: content checklist passed

merge-gate-audit documentation content check
source: docs/merge-gate-audit.md

PASS both_endpoints: An audit MUST query both endpoints and combine the answers.
PASS rulesets_endpoint: Repository rulesets
PASS branch_protection_endpoint: Classic branch protection
PASS statuses_wrong: The GitHub Statuses API is the wrong endpoint for GitHub Actions results.
PASS check_runs: /commits/<ref>/check-runs
PASS proof_failure: mergeable_state: blocked
PASS proof_success: mergeable_state: clean
PASS fleet_standard: The chosen fleet standard for the required-check gate is the repository ruleset with no bypass actors.
PASS hlr_research_conversion: hlr-research` was converted
PASS no_bypass: bypass_actors: []
PASS push_restrictions: Push restriction (10 of 13)
PASS enforce_admins_decision: Whether to set `enforce_admins: true` on the ten push restrictions is a separate decision

result: content checklist passed
Evidence: Changed-file test runner summary
{
  "families": [],
  "finished_at": "2026-08-05T12:39:32Z",
  "run_id": "empty",
  "scripts": [],
  "selection": "changed:base=7778ca3246868064e4e33531174a5032f705fb76",
  "started_at": "2026-08-05T12:39:32Z",
  "summary": {
    "duration_ms": 0,
    "failed": 0,
    "skipped_gate": 0,
    "total": 0
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • git diff --check 7778ca3246868064e4e33531174a5032f705fb76..9208246ad1bd53c7562d357b6148db9ca1fd597f
  • bin/fm-test-run.sh --changed --base 7778ca3246868064e4e33531174a5032f705fb76 --json /tmp/no-mistakes-evidence/01KZ8YT6P2XXW7VTKNBZTW2YTD/fm-test-run-changed.json
  • Content checklist against docs/merge-gate-audit.md, written to /tmp/no-mistakes-evidence/01KZ8YT6P2XXW7VTKNBZTW2YTD/merge-gate-audit-content-check.txt
  • Rendered docs/merge-gate-audit.md to /tmp/no-mistakes-evidence/01KZ8YT6P2XXW7VTKNBZTW2YTD/merge-gate-audit-render.html
  • if grep -n $'—' docs/merge-gate-audit.md; then exit 1; fi
  • git status --short
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Crew Agent added 2 commits August 5, 2026 12:34
…es live

Convert hlr-research's required-check gate from classic branch protection to
a no-bypass repository ruleset, making all 13 fleet required-check gates
uniform on rulesets. Document the two-mechanism reality, the rule that an
audit MUST query both endpoints (push restrictions remain classic-only on 10
repos), how to read Actions results via check-runs, how to prove a gate, and
the bypass picture for both mechanisms.
@Freudator86
Freudator86 merged commit 88b8ac6 into main Aug 6, 2026
10 checks passed
@Freudator86
Freudator86 deleted the fm/hlr-fleet-ci-gate-mechanism-consistency branch August 6, 2026 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant