diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index b3a5fffded1..8707b2d6a75 100644 --- a/actions/setup/js/error_recovery.cjs +++ b/actions/setup/js/error_recovery.cjs @@ -37,7 +37,15 @@ const DEFAULT_RETRY_CONFIG = { * @returns {boolean} True if the error is transient and should be retried */ function isTransientError(error) { - const errorMsg = getErrorMessage(error).toLowerCase(); + const errorMsg = getErrorMessage(error); + const errorMsgLower = errorMsg.trimStart().toLowerCase(); + + // GitHub REST APIs may crash and return an HTML error page (e.g. the "Unicorn!" + // 500 page) instead of JSON. Detect this by checking for an HTML doctype at the + // start of the error message and treat it as a transient server error. + if (errorMsgLower.startsWith(" errorMsg.includes(pattern)); + return transientPatterns.some(pattern => errorMsgLower.includes(pattern)); } /** diff --git a/actions/setup/js/error_recovery.test.cjs b/actions/setup/js/error_recovery.test.cjs index 45c4bacf261..3d62889b7ea 100644 --- a/actions/setup/js/error_recovery.test.cjs +++ b/actions/setup/js/error_recovery.test.cjs @@ -42,6 +42,13 @@ describe("error_recovery", () => { expect(isTransientError(new Error("no server is currently available"))).toBe(true); }); + it("should identify HTML responses (GitHub 500 Unicorn page) as transient", () => { + expect(isTransientError(new Error("Unicorn!"))).toBe(true); + expect(isTransientError(new Error("\n..."))).toBe(true); + // With leading whitespace + expect(isTransientError(new Error(" ..."))).toBe(true); + }); + it("should not identify validation errors as transient", () => { expect(isTransientError(new Error("Invalid input"))).toBe(false); expect(isTransientError(new Error("Field is required"))).toBe(false); diff --git a/actions/setup/js/update_handler_factory.cjs b/actions/setup/js/update_handler_factory.cjs index 25cc21f54f7..fae139d3205 100644 --- a/actions/setup/js/update_handler_factory.cjs +++ b/actions/setup/js/update_handler_factory.cjs @@ -11,6 +11,7 @@ const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); +const { withRetry, isTransientError } = require("./error_recovery.cjs"); /** * @typedef {Object} UpdateHandlerConfig @@ -253,8 +254,9 @@ function createUpdateHandlerFactory(handlerConfig) { // Execute the update using the authenticated client and effective context. // githubClient uses config["github-token"] when set (for cross-repo), otherwise global github. // effectiveContext.repo contains the target repo owner/name for cross-repo routing. + // Retry on transient errors (e.g. GitHub API returning HTML instead of JSON on 500 crashes). try { - const updatedItem = await executeUpdate(githubClient, effectiveContext, itemNumber, updateData); + const updatedItem = await withRetry(() => executeUpdate(githubClient, effectiveContext, itemNumber, updateData), { maxRetries: 1, initialDelayMs: 2000, shouldRetry: isTransientError }, `update ${itemTypeName} #${itemNumber}`); core.info(`Successfully updated ${itemTypeName} #${itemNumber}: ${updatedItem.html_url || updatedItem.url}`); // Format and return success result diff --git a/actions/setup/js/update_handler_factory.test.cjs b/actions/setup/js/update_handler_factory.test.cjs index 6ad00d8abde..3e55ad3980e 100644 --- a/actions/setup/js/update_handler_factory.test.cjs +++ b/actions/setup/js/update_handler_factory.test.cjs @@ -314,7 +314,7 @@ describe("update_handler_factory.cjs", () => { const result = await handler({ title: "Test" }); expect(result.success).toBe(false); - expect(result.error).toBe("API Error"); + expect(result.error).toContain("API Error"); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to update test item")); });