[chore] logical-gaps: release approve async dispatch (202) — kill the 40min synchronous HTTP 504

The approve route ran the whole fail-closed execute inline: clone(600s) +
gate(1800s) + CI poll(2400s) + publish(300s) ≈ up to 85min worst case. nginx
(the single :3000 entry point, ~60s read timeout) 504'd long before it
finished, so the CEO's approve always appeared to fail even when the release
succeeded server-side — the structured ReleaseResult was unreachable over the
wire. dispatch_approve spawns the execute in a background task with a fresh
session (built from the request session's engine) and the route returns 202
'accepted' immediately; _INFLIGHT_APPROVES tracks the dispatched task for
observability (self-cleans via done-callback; the Redis mutex still refuses a
double-execute on a second click). The panel already polls GET /proposal every
30s, so it observes the final status (COMPLETED on published/already_published,
else the proposal stays open for retry); the card's approve toast now treats
'accepted' as an info 'dispatched, running in the background' instead of the
old 'Release halted' warning.

TDD: 2 route tests red→green (approve returns 202 'accepted' + the proposal
transitions to COMPLETED / stays PENDING once the background faked execute
completes; the dispatched task is awaited while the executor patch is live).
83 release tests green; ruff/mypy clean; panel typecheck+lint+format+test
green.
This commit is contained in:
Renn F
2026-06-30 18:24:54 +02:00
parent 05616607e4
commit 0bf6c8484e
4 changed files with 177 additions and 34 deletions
@@ -65,6 +65,14 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
`Published v${result.version}` +
(result.release_url ? "" : " (no release URL returned)"),
);
} else if (result.status === "accepted") {
// The execute runs in the background (a synchronous request would 504
// at nginx before the ~40min fail-closed gate/CI/publish finished).
// This card polls GET /proposal every 30s and reflects the final
// outcome (COMPLETED on a publish, else the proposal stays open).
toast.info(
"Release execute dispatched — running in the background. This card updates as it progresses.",
);
} else {
toast.warning(`Release halted (${result.status}): ${result.detail}`);
}