diff --git a/scripts/release.mjs b/scripts/release.mjs index 12e8660d..bbcc2f66 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -558,18 +558,40 @@ const parseRunId = (output) => { return match ? Number(match[1]) : null; }; -const waitForRun = async ({ repository, runId, label }) => { +export const waitForRun = async ({ + repository, + runId, + label, + viewRun = () => ghJson([ + "run", + "view", + String(runId), + "--repo", + repository, + "--json", + "status,conclusion,url,headSha", + ]), + waitForNextPoll = () => wait(POLL_INTERVAL_MS), + maxConsecutiveFailures = 5, +}) => { let lastStatus = ""; + let consecutiveFailures = 0; while (true) { - const runView = ghJson([ - "run", - "view", - String(runId), - "--repo", - repository, - "--json", - "status,conclusion,url,headSha", - ]); + let runView; + try { + runView = viewRun(); + consecutiveFailures = 0; + } catch (error) { + consecutiveFailures += 1; + if (consecutiveFailures >= maxConsecutiveFailures) { + throw error; + } + console.warn( + `[release] ${label}: GitHub status check failed; retrying (${consecutiveFailures}/${maxConsecutiveFailures - 1})`, + ); + await waitForNextPoll(); + continue; + } const statusLabel = `${runView.status}${runView.conclusion ? `/${runView.conclusion}` : ""}`; if (statusLabel !== lastStatus) { console.log(`[release] ${label}: ${statusLabel} (${runView.url})`); @@ -581,7 +603,7 @@ const waitForRun = async ({ repository, runId, label }) => { } return runView; } - await wait(POLL_INTERVAL_MS); + await waitForNextPoll(); } }; diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs index ddcffe8f..c3483851 100644 --- a/scripts/release.test.mjs +++ b/scripts/release.test.mjs @@ -12,6 +12,7 @@ import { resolveReleaseVersion, reusedAssetMatches, selectPublishedDmg, + waitForRun, } from "./release.mjs"; describe("release automation", () => { @@ -26,6 +27,28 @@ describe("release automation", () => { }); }); + test("retries transient GitHub status failures while waiting for a workflow", async () => { + let attempts = 0; + let polls = 0; + const result = await waitForRun({ + repository: "tianma-if/edgeever", + runId: 123, + label: "Draft assets", + viewRun: () => { + attempts += 1; + if (attempts < 3) throw new Error("transient EOF"); + return { status: "completed", conclusion: "success", url: "https://example.test/run" }; + }, + waitForNextPoll: async () => { + polls += 1; + }, + }); + + expect(result.conclusion).toBe("success"); + expect(attempts).toBe(3); + expect(polls).toBe(2); + }); + test("parses paired bilingual changes and labels", () => { expect( parseReleaseArgs([