import { loadPullByIssueId, MergeError } from "../pulls"; import { hasPermission, loadRepoById, repoPermission } from "../repos"; import { loadProtections, protectionFor } from "../protection"; import { listCommitStatuses, requiredCheckResults } from "../statuses"; import { enqueuePull } from "../merge-queue"; import { actorViewer } from "../merge-automation"; import { ciVerdict, MAX_ENQUEUE_ATTEMPTS } from "./requeue-policy"; import { loadRun, updateRun } from "./store"; export type RequeueStep = "done" | "wait"; /** Ends a requeue that can no longer progress (for example after an unexpected error), so it never shows as waiting. */ export async function abandonRequeue(runId: string, reason: string): Promise { await updateRun(runId, ["applied"], { requeue_status: "gave_up", requeue_error: reason }); } async function giveUp(runId: string, reason: string): Promise { await updateRun(runId, ["applied"], { requeue_status: "gave_up", requeue_error: reason }); return "done"; } /** * One poll of a queue-triggered fix: once the fix commit is green the pull goes back into the merge queue * as the user who queued it. Red CI, a moved head or repeated enqueue failures end the attempt. */ export async function checkRequeue(runId: string, poll: number, final = false): Promise { const runRow = await loadRun(runId); if (!runRow || runRow.status !== "applied" || runRow.requeue_status !== "waiting") return "done"; if (!runRow.queue_method || !runRow.commit_sha) return giveUp(runId, "Nothing to re-enqueue"); const pull = await loadPullByIssueId(runRow.repo_id, runRow.pull_issue_id); if (!pull || pull.state !== "open") return giveUp(runId, "The pull request is no longer open"); if (pull.head_sha !== runRow.commit_sha) return giveUp(runId, "The head branch changed after the fix"); if (pull.draft === 1) return giveUp(runId, "The pull request was converted to a draft"); const [baseRepo, actor] = await Promise.all([loadRepoById(pull.repo_id), actorViewer(runRow.actor_id)]); if (!baseRepo || !actor) return giveUp(runId, "The repository or the user who queued the pull is gone"); if (!hasPermission(await repoPermission(baseRepo, actor), "write")) { return giveUp(runId, "The user who queued the pull can no longer merge it"); } const applierId = runRow.applied_by ?? runRow.actor_id; const applier = applierId ? await actorViewer(applierId) : null; if (!applier || !hasPermission(await repoPermission(baseRepo, applier), "write")) { return giveUp(runId, "The fix was applied by someone who can't merge into the base repository"); } const required = protectionFor(await loadProtections(baseRepo.id), pull.base_ref)?.requiredChecks ?? []; const statuses = await listCommitStatuses(pull.head_repo_id, runRow.commit_sha); const verdict = ciVerdict({ required, requiredResults: await requiredCheckResults(baseRepo, runRow.base_sha, statuses, required), statuses, poll, }); if (verdict.kind === "red") return giveUp(runId, verdict.reason); if (verdict.kind === "wait") return final ? giveUp(runId, "Timed out waiting for CI on the fix") : "wait"; try { await enqueuePull(pull, runRow.queue_method, actor); } catch (error) { if (error instanceof MergeError && error.message.includes("already in the merge queue")) { await updateRun(runId, ["applied"], { requeue_status: "enqueued" }); return "done"; } const attempts = runRow.requeue_attempts + 1; const reason = error instanceof Error ? error.message : "Could not re-enqueue"; if (attempts >= MAX_ENQUEUE_ATTEMPTS || final) { await updateRun(runId, ["applied"], { requeue_attempts: attempts }); return giveUp(runId, reason); } await updateRun(runId, ["applied"], { requeue_attempts: attempts, requeue_error: reason }); return "wait"; } await updateRun(runId, ["applied"], { requeue_status: "enqueued", requeue_error: null }); return "done"; }