fix(git-tools): add admin-gated --no-ci-expected merge assertion to ci-queue-wait

A repository with no CI configured has no sanctioned wrapper merge
path: the queue guard fails closed on zero status contexts for
purpose=merge (exit 3). That fail-closed default stays correct, because
at merge time no-status may also mean CI has not reported yet; but it
left CI-less repos unmergeable without the break-glass override, which
makes the override a gap rather than an exception.

--no-ci-expected reclassifies only the zero-context case for merge as
queue-clear, and only when the acting token holds repository admin.
The elevation check reads the repository object's permissions.admin:
the branch-head and combined-status responses the guard already
fetches carry no permissions object at all. The assertion prints its
own audit line (purpose, branch, asserting identity, reason) and
writes a NO_CI_ASSERTED JSONL record to the existing audit sink; an
unauditable pass is refused (exit 70). A non-admin caller is refused
with exit 77 (ASSERTION_REFUSED, own text, audited), kept distinct
from ASSERTED_NOT_READY's exit 3; an unavailable permissions lookup
holds as CANNOT_ASSERT exit 75. --require-status contradicts the flag
and is a usage error; a pending or failed context still holds or
fails exactly as before; push behavior is unchanged.

pr-merge.sh gains a pass-through --no-ci-expected that only forwards
the flag to the guard invocation. PowerShell twins are unchanged: no
existing test exercises their guard path (pr-merge.ps1 only runs with
-SkipQueueGuard; ci-queue-wait.ps1 has no test).

Closes #1372
This commit is contained in:
2026-08-23 12:34:11 -05:00
parent 24294d3b77
commit 28f4002a70
6 changed files with 524 additions and 11 deletions
@@ -111,7 +111,7 @@ approve path carries the trap.) `pr-review.sh` sends the correct token for the d
Whatever you use, re-read `GET /pulls/{n}/reviews` and assert the state before reporting a verdict
placed.
The guard exits nonzero for any provider-asserted non-green, missing, or malformed CI state. If credentials or the provider are unavailable, it emits `CANNOT_ASSERT` and writes a JSONL audit record. Push degrades to exit 0 so recovery work is not bricked; merge holds with retryable exit 75 until the provider recovers, then self-clears without manual reset. Neither outcome is evidence that CI was clear. `pr-merge.sh` automatically inspects the exact PR head repository and full commit SHA rather than its `main` base; this also handles fork PRs without branch-name ambiguity. Pass `--expect-head <approved-full-sha>` to bind a commit-specific review or merge-gate verdict; Gitea uses atomic `head_commit_id` and GitHub uses `--match-head-commit`.
The guard exits nonzero for any provider-asserted non-green, missing, or malformed CI state. If credentials or the provider are unavailable, it emits `CANNOT_ASSERT` and writes a JSONL audit record. Push degrades to exit 0 so recovery work is not bricked; merge holds with retryable exit 75 until the provider recovers, then self-clears without manual reset. Neither outcome is evidence that CI was clear. For a repository with no CI configured at all, `pr-merge.sh --no-ci-expected` is the sanctioned merge path: it forwards to `ci-queue-wait.sh --no-ci-expected`, which reclassifies a zero-context merge head as queue-clear only when the acting token holds repository admin, and records the assertion (or its refusal) in the same JSONL audit log. `pr-merge.sh` automatically inspects the exact PR head repository and full commit SHA rather than its `main` base; this also handles fork PRs without branch-name ambiguity. Pass `--expect-head <approved-full-sha>` to bind a commit-specific review or merge-gate verdict; Gitea uses atomic `head_commit_id` and GitHub uses `--match-head-commit`.
### Code Review (Codex)