Parent: #1936
Problem
Unlike executeAgentMaintenanceActions, which calls fetchPullRequestFreshness and compares live state before any write, maybeRecloseDisallowedReopen goes straight from its permission/closer checks to closePullRequest with no live GET first. The pr object it reads is the just-upserted webhook payload, which is fresh on the synchronous first attempt — but github-webhook queue jobs are retried on failure (up to 3 retries, 30s delay per attempt) using the originally captured payload, not a re-fetch. Related: see the sibling issue filed alongside this one covering this same function's swallowed-error/false-audit-outcome gap.
Failure scenario: a non-maintainer reopens a PR; the reopen webhook is queued but a transient failure elsewhere forces a queue retry. Between the original delivery and the retry (up to ~90s+ later), a maintainer manually reopens and approves the PR — a legitimate override. The retried job still re-closes it, since closePullRequest has no head-SHA/state precondition and the handler never re-reads current GitHub state before writing, silently overwriting the maintainer's live decision.
Requirements
- A retried reopen-reclose job must reflect the PR's live state at the moment of the mutation, not the payload captured at original delivery.
Deliverables
- Add a
fetchPullRequestFreshness-style live re-check immediately before the closePullRequest call in this handler, denying the close if the PR's live state no longer matches the reopen that triggered it (e.g. a maintainer has since re-approved/re-opened it deliberately).
- Add a regression test: the reopen job is retried after a maintainer's legitimate override — assert the retried close is denied, not executed.
Acceptance criteria
- A retried reopen-reclose job that would overwrite a legitimate maintainer override since the original delivery is denied.
- The common, non-retried case is unaffected.
Expected outcome
The reopen-reclose path gets the same live-freshness guarantee every other actuation path already has, closing a narrow but real override-clobbering race introduced by queue retries.
Parent: #1936
Problem
Unlike
executeAgentMaintenanceActions, which callsfetchPullRequestFreshnessand compares live state before any write,maybeRecloseDisallowedReopengoes straight from its permission/closer checks toclosePullRequestwith no live GET first. Theprobject it reads is the just-upserted webhook payload, which is fresh on the synchronous first attempt — butgithub-webhookqueue jobs are retried on failure (up to 3 retries, 30s delay per attempt) using the originally captured payload, not a re-fetch. Related: see the sibling issue filed alongside this one covering this same function's swallowed-error/false-audit-outcome gap.Failure scenario: a non-maintainer reopens a PR; the reopen webhook is queued but a transient failure elsewhere forces a queue retry. Between the original delivery and the retry (up to ~90s+ later), a maintainer manually reopens and approves the PR — a legitimate override. The retried job still re-closes it, since
closePullRequesthas no head-SHA/state precondition and the handler never re-reads current GitHub state before writing, silently overwriting the maintainer's live decision.Requirements
Deliverables
fetchPullRequestFreshness-style live re-check immediately before theclosePullRequestcall in this handler, denying the close if the PR's live state no longer matches the reopen that triggered it (e.g. a maintainer has since re-approved/re-opened it deliberately).Acceptance criteria
Expected outcome
The reopen-reclose path gets the same live-freshness guarantee every other actuation path already has, closing a narrow but real override-clobbering race introduced by queue retries.