Files
ersatztv/scripts/tests
timothyandClaude Opus 5 afe61e80aa fix(845): the verdict writer checks that the gate will honour what it just posted
`.gitea/workflows/review-verdict.yml` inherits an existing `review-verdict/h10=success`
only from a status whose `.creator.login` is on its `H10_REVIEWERS` allow-list (#742).
`scripts/post-review-verdict.sh` writes those verdicts with whatever account owns
`ETV_GITEA_TOKEN`/`ETV_GITEA_BASICAUTH` and never asked whose it was. The two values were
coupled with nothing asserting the coupling, and the failure mode was the silent one: the
status really is written, the tool really does report success, and then the next
`pull_request_target` event re-derives it and posts over it. And again. The PR deadlocks
and the only diagnostic is a `::warning::` inside a workflow run nobody is reading.

The script now READS THE STATUS BACK after posting it, identifies its own write by state
and description, and refuses — before the comment, so the surviving half-state is the
documented `ask` one — unless the recorded creator is on the allow-list. Measured after
the write rather than probed before it: a pre-flight `GET /user` tests what the credential
claims to be, this tests what Gitea recorded as the author of this status, which is the
value the gate reads. It also needs no scope beyond the repo access the POST already
required.

The allow-list is not restated. `scripts/lib/h10-reviewers.sh` derives it from the
workflow's own literal, so there is ONE declaration rather than two held together by a
parity test — the shape `testing.guard-derives-population-from-source` records six failed
rounds against. It is a parse rather than a shared declaration both sides source, and that
is not a preference: the gate runs against a checkout of the PR's BASE sha, so a PR whose
base predates a new `scripts/lib/` file would not have it, and a missing `source` under
`set -euo pipefail` kills the job — which posts no `review-verdict/h10` at all and blocks
every merge to `main` including the PR that would repair it, with no admin override (#743).
A fallback list for that case reintroduces the second copy. So the derivation runs on the
side that can fail safely and the gate's body is untouched.

Fail-closed on every branch of the derivation: 0 or >1 anchored assignments, an entry that
is not a plain login, an empty list, or an unreadable file all refuse BEFORE any network
call. The membership query answers member / not-a-member / cannot-tell as three distinct
codes rather than two, so a caller cannot fold "cannot tell" into either.

The safety DIRECTION is unchanged and the commit does not claim otherwise: a
non-allow-listed verdict was always a stall, never a fail-open. What changes is where the
signal lands — at the terminal that made the mistake, instead of one PR event later.

Residual, stated rather than fixed: the check runs after the POST, so a refused verdict
leaves a non-inheritable status standing with no comment beside it. That is the `ask`
half-state `release.verdict-writes-status-before-comment` already designates as the safe
one, and the gate re-derives it on the next PR event. A second corrective write is the
sticky-sentinel mechanism #849 is separately designing; inventing a parallel one here would
be two mechanisms for one invariant.

`scripts/post-review-verdict.sh` moves BEHAVIOUR-ONLY -> MUTATION in the guard inventory,
which the manifest's own UNDECLARED note called "the most valuable upgrade on this list".
The declared clause is in the GATE, not the script: rewriting `H10_REVIEWERS` while the
posting account stays fixed reddens the accept path only if the writer reads the list live
AND the comparison gates the outcome. Disarming the comparison instead would prove the
second and say nothing about the first, which is the half that was missing.

Verification: full `scripts/tests` suite green (1239 passed, 2 skipped); the declared
mutation executes and reddens its named proof with the declared diagnostic; four clauses
disarmed by hand and each confirmed to redden its own test.

fixes #845

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019zUmJZHhVP7kXg5DV237TW
2026-08-30 03:38:47 +02:00
..