diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 93b4638eadd..2ddb0cc8e14 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1347,6 +1347,31 @@ function buildTimeoutContext(isTimedOut, timeoutMinutes) { return "\n" + renderTemplateFromFile(templatePath, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes }); } +/** + * Determine whether engine-failure context should be included. + * Timeout outcomes should rely on dedicated timeout messaging instead. + * @param {string} agentConclusion + * @param {boolean} hasToolDenialsExceeded + * @param {boolean} isTimedOut + * @returns {boolean} + */ +function shouldBuildEngineFailureContext(agentConclusion, hasToolDenialsExceeded, isTimedOut) { + return agentConclusion === "failure" && !hasToolDenialsExceeded && !isTimedOut; +} + +/** + * Determine whether issue create/update failed due to token permission limits. + * @param {unknown} error + * @returns {boolean} + */ +function isIssueWritePermissionError(error) { + /** @type {{status?: unknown} | null} */ + const typedError = error && typeof error === "object" ? error : null; + const status = Number(typedError?.status); + const message = getErrorMessage(error).toLowerCase(); + return status === 403 && (message.includes("resource not accessible by integration") || message.includes("resource not accessible by personal access token") || message.includes("insufficient permissions")); +} + /** * Build a context string when the Copilot CLI failed due to the token lacking inference access. * @param {boolean} hasInferenceAccessError - Whether an inference access error was detected @@ -2724,7 +2749,7 @@ async function main() { // Suppress when tool-denials-exceeded is present: the engine termination is a // direct consequence of the SDK hitting the denial threshold, so the tool-denials // context is the more actionable signal. - const engineFailureContext = agentConclusion === "failure" && !hasToolDenialsExceeded ? buildEngineFailureContext({ suppressEngineRateLimit429: maxAICreditsExceeded }) : ""; + const engineFailureContext = shouldBuildEngineFailureContext(agentConclusion, hasToolDenialsExceeded, isTimedOut) ? buildEngineFailureContext({ suppressEngineRateLimit429: maxAICreditsExceeded }) : ""; // Build timeout context const timeoutContext = buildTimeoutContext(isTimedOut, timeoutMinutes); @@ -2948,7 +2973,7 @@ async function main() { // Suppress when tool-denials-exceeded is present: the engine termination is a // direct consequence of the SDK hitting the denial threshold, so the tool-denials // context is the more actionable signal. - const engineFailureContext = agentConclusion === "failure" && !hasToolDenialsExceeded ? buildEngineFailureContext({ suppressEngineRateLimit429: maxAICreditsExceeded }) : ""; + const engineFailureContext = shouldBuildEngineFailureContext(agentConclusion, hasToolDenialsExceeded, isTimedOut) ? buildEngineFailureContext({ suppressEngineRateLimit429: maxAICreditsExceeded }) : ""; // Build timeout context const timeoutContext = buildTimeoutContext(isTimedOut, timeoutMinutes); @@ -3077,7 +3102,11 @@ async function main() { await detectAndHandleFailureCascade(owner, repo, newIssue.data.number); } } catch (error) { - core.warning(`Failed to create or update failure tracking issue: ${getErrorMessage(error)}`); + if (isIssueWritePermissionError(error)) { + core.info(`Skipping failure tracking issue creation/update: token lacks issues:write permission (${getErrorMessage(error)})`); + } else { + core.warning(`Failed to create or update failure tracking issue: ${getErrorMessage(error)}`); + } // Don't fail the workflow if we can't create the issue } } catch (error) { @@ -3095,6 +3124,8 @@ module.exports = { buildStaleLockFileFailedContext, buildDailyAICExceededContext, buildTimeoutContext, + shouldBuildEngineFailureContext, + isIssueWritePermissionError, buildAssignCopilotFailureContext, buildEngineFailureContext, buildReportIncompleteContext, diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index e0ffea4b132..83a2f2e35e5 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -1462,6 +1462,47 @@ describe("handle_agent_failure", () => { }); }); + describe("shouldBuildEngineFailureContext", () => { + const { shouldBuildEngineFailureContext } = require("./handle_agent_failure.cjs"); + + it("returns true for plain failure without timeout or tool-denials-exceeded", () => { + expect(shouldBuildEngineFailureContext("failure", false, false)).toBe(true); + }); + + it("returns false when timeout is detected", () => { + expect(shouldBuildEngineFailureContext("failure", false, true)).toBe(false); + }); + + it("returns false when tool-denials-exceeded is present", () => { + expect(shouldBuildEngineFailureContext("failure", true, false)).toBe(false); + }); + + it("returns false for non-failure conclusions", () => { + expect(shouldBuildEngineFailureContext("timed_out", false, true)).toBe(false); + expect(shouldBuildEngineFailureContext("success", false, false)).toBe(false); + }); + }); + + describe("isIssueWritePermissionError", () => { + const { isIssueWritePermissionError } = require("./handle_agent_failure.cjs"); + + it("returns true for 403 Resource not accessible by integration", () => { + expect(isIssueWritePermissionError({ status: 403, message: "Resource not accessible by integration" })).toBe(true); + }); + + it("returns true for 403 insufficient permissions", () => { + expect(isIssueWritePermissionError({ status: 403, message: "Insufficient permissions to create issue" })).toBe(true); + }); + + it("returns true for 403 resource not accessible by personal access token", () => { + expect(isIssueWritePermissionError({ status: 403, message: "Resource not accessible by personal access token" })).toBe(true); + }); + + it("returns false for non-403 errors", () => { + expect(isIssueWritePermissionError({ status: 500, message: "Internal server error" })).toBe(false); + }); + }); + // ────────────────────────────────────────────────────── // buildEngineFailureContext // ──────────────────────────────────────────────────────