From 62dc9f866a13db3538ebde849293313e68841daf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:51:01 +0000 Subject: [PATCH 1/3] Initial plan From 1a748b6c3a11635313c46d65e31a267b3ef4601b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:02:52 +0000 Subject: [PATCH 2/3] fix: retry GitHub REST API calls when HTML is returned instead of JSON Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e99d386d-c5e4-4c8d-a8ed-98bba745705b --- actions/setup/js/error_recovery.cjs | 12 ++++++++++-- actions/setup/js/error_recovery.test.cjs | 7 +++++++ actions/setup/js/update_handler_factory.cjs | 4 +++- actions/setup/js/update_handler_factory.test.cjs | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index b3a5fffded1..06aeb102464 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.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 (errorMsg.trimStart().toLowerCase().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")); }); From 7426e68fbb51232c5648d2c53e10a964c0f94de7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:09:33 +0000 Subject: [PATCH 3/3] refactor: optimize isTransientError to avoid redundant string processing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e99d386d-c5e4-4c8d-a8ed-98bba745705b --- actions/setup/js/error_recovery.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index 06aeb102464..8707b2d6a75 100644 --- a/actions/setup/js/error_recovery.cjs +++ b/actions/setup/js/error_recovery.cjs @@ -38,12 +38,12 @@ const DEFAULT_RETRY_CONFIG = { */ function isTransientError(error) { const errorMsg = getErrorMessage(error); - const errorMsgLower = errorMsg.toLowerCase(); + 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 (errorMsg.trimStart().toLowerCase().startsWith("