#772 — the pinned CI toolchain image can be deleted out from under us, and when it was
(2026-08-11..13) all five `container:` jobs died at image pull, both required contexts
included, with the cause buried in each job's log. Root cause is registry-side and is now
established rather than guessed: an owner-level Gitea package cleanup rule (keep_count 15,
remove_days 1, remove_pattern `.*`, keep_pattern no 7-hex sha can match) deletes a sha tag
once 15 newer versions exist, and `ExecuteCleanupRules` ran nightly through the window. The
`ersatztv` package carries the same rule's fingerprint exactly — every sha tag older than
the 15-slot window is gone, every keep_pattern tag back to 26.3.1 survives. Version deletes
leave no audit row, so the specific run cannot be replayed; that limit is stated where the
claim is made. The durable fix belongs to the registry's repo: server-management#842.
What lands here is what a consumer of someone else's registry can do:
* `toolchain-preflight`, a container-free job (a job consuming the image could not run to
report it missing) resolving every pin against the registry and failing with a message
that names the tag and the recovery. Not a `needs:` of the jobs it diagnoses — gating
five jobs behind a checkout and one curl taxes every green run to speed up a rare red
one, and they already fail fast.
* Only HTTP 404 means gone. Everything else is could-not-tell, and rejected credentials
fail rather than pass as unknown — "the check could not run" must never present as
"the pin is fine".
* A recovery path that does not need CI: rebuild the SAME tag from the commit it names
and push it. The push half was verified against this registry on 2026-08-22 with a
throwaway package (created, resolved 200, deleted).
#792 — the reported defect was the exit code, and re-measuring says that premise is false:
every no-status path already exits 1, and eight refusal modes now assert it against the real
predecessor, where they pass. The observed 0 came from the invocation, not the script. What
WAS broken is the half-state the issue describes second: the comment was written before the
status, so every refusal left `Review-verdict: MERGEABLE @ <head>` on a PR with no gating
status behind it. The two writes are now ordered status-then-comment, which makes the only
reachable half-state the safe one — a status with no comment leaves the merge hook's
condition (c) with nothing to classify, which is an `ask`. The refusals themselves are
untouched. Ordering rather than compensating deletion: an orphaned-comment cleanup needs a
Gitea call, and these refusals are usually caused by Gitea being unreachable.
Proof for the ordering is the split against origin/main's script: the 8 orphan/ordering
tests go red there, the 8 exit-code tests stay green.
fixes#772fixes#792
Refs: server-management#842
Decisions-Edit: yes