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"));
});