From 2bd97791c963b09fd01eee622c55a344e46cfeb5 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 31 May 2026 16:13:52 -0700 Subject: [PATCH] feat(fleet): resilient PR merge on ship (inline attempt + background retry) The corporate proxy intermittently 407s GitHub's API, so a single gh pr merge can fail transiently. Try once inline (fast path), then retry in the background with backoff (3s/8s/20s/45s) without blocking the ship; mark prState=merged when one lands. Best-effort throughout. --- .../src/modules/fleet/coordinator.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/services/platform-service/src/modules/fleet/coordinator.ts b/services/platform-service/src/modules/fleet/coordinator.ts index 3d985711..8f46566f 100644 --- a/services/platform-service/src/modules/fleet/coordinator.ts +++ b/services/platform-service/src/modules/fleet/coordinator.ts @@ -643,17 +643,44 @@ const execFileAsync = promisify(execFile); * authenticated (the coordinator host). Best-effort: a failure leaves the PR open * and never fails the ship. Marks the run prState=merged on success. */ +async function ghMergePr(prUrl: string): Promise { + try { + await execFileAsync('gh', ['pr', 'merge', prUrl, '--squash', '--delete-branch'], { + timeout: 60_000, + }); + return true; + } catch { + // Network/proxy hiccup (e.g. intermittent 407), checks pending, or conflict. + return false; + } +} + async function mergeRunPrOnShip(jobId: string, latest: FleetRunDoc | undefined): Promise { if (process.env.FLEET_SHIP_MERGES_PR !== '1') return; if (!latest?.prUrl || latest.prState === 'merged') return; - try { - await execFileAsync('gh', ['pr', 'merge', latest.prUrl, '--squash', '--delete-branch'], { - timeout: 60_000, - }); - await repo.updateRun(latest.id, jobId, { prState: 'merged' }); - } catch { - // best-effort — leave the PR open if the merge is blocked (checks, conflicts…) + const { prUrl, id: runId } = latest; + // Fast attempt inline (so a healthy proxy merges immediately without delaying ship). + if (await ghMergePr(prUrl)) { + await repo.updateRun(runId, jobId, { prState: 'merged' }); + return; } + // The corporate proxy intermittently 407s GitHub's API. Retry in the BACKGROUND + // with backoff so the ship response is never blocked; mark merged when one lands. + void (async () => { + for (const delayMs of [3_000, 8_000, 20_000, 45_000]) { + await new Promise(r => setTimeout(r, delayMs)); + if (await ghMergePr(prUrl)) { + try { + await repo.updateRun(runId, jobId, { prState: 'merged' }); + } catch { + /* run gone — ignore */ + } + return; + } + } + })().catch(() => { + /* detached best-effort — never throws */ + }); } export async function patchJobFenced(