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("