diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 9263ec31db2..1e415fc9c8e 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -399,7 +399,17 @@ async function main(config = {}) { const errorType = isAuthError ? "authentication/permission" : "agent availability"; core.warning(`Agent assignment failed for ${agentName} on ${type} #${number} due to ${errorType} error. Skipping due to ignore-if-error=true.`); core.info(`Error details: ${errorMessage}`); - _allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true, skipped: true }); + _allResults.push({ + issue_number: issueNumber, + pull_number: pullNumber, + agent: agentName, + owner: effectiveOwner, + repo: effectiveRepo, + pull_request_repo: effectivePullRequestRepoSlug, + success: true, + skipped: true, + error: errorMessage, + }); return { success: true, skipped: true }; } @@ -451,18 +461,24 @@ function getAssignToAgentAssigned() { /** * Returns the "assignment_errors" output string for step outputs. - * Format: "issue:N:agent:error" or "pr:N:agent:error" per failure, newline-separated. + * Format: "issue:N:agent:error" or "pr:N:agent:error" per failure/skipped-with-error, + * newline-separated. * @returns {string} */ function getAssignToAgentErrors() { - return _allResults - .filter(r => !r.success && !r.skipped) - .map(r => { - const number = r.issue_number || r.pull_number; - const prefix = r.issue_number ? "issue" : "pr"; - return `${prefix}:${number}:${r.agent}:${r.error}`; - }) - .join("\n"); + return ( + _allResults + // Include skipped(ignore-if-error) entries that still captured an error so + // downstream failure handling can surface assignment problems in issue/comment reports. + // Include hard failures (!success) and ignored failures (skipped=true with error). + .filter(r => r.error && (r.skipped || !r.success)) + .map(r => { + const number = r.issue_number || r.pull_number; + const prefix = r.issue_number ? "issue" : "pr"; + return `${prefix}:${number}:${r.agent}:${r.error}`; + }) + .join("\n") + ); } /** diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index d2ae930db8f..1c9b80e84f2 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -1054,6 +1054,8 @@ describe("assign_to_agent", () => { // Should not fail the workflow expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.setOutput).toHaveBeenCalledWith("assignment_error_count", "0"); + expect(mockCore.setOutput).toHaveBeenCalledWith("assignment_errors", expect.stringContaining("Bad credentials")); // Summary should show skipped assignments expect(mockCore.summary.addRaw).toHaveBeenCalled(); diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 08d45e45dd0..c71ad6a8c72 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -2030,6 +2030,40 @@ function buildCredentialAuthErrorContext(auditJsonlPathOverride) { const templatePath = getPromptPath("credential_auth_error.md"); return "\n" + renderTemplateFromFile(templatePath, { providers: providersList }); } + +/** + * Build a context string when assign_to_agent reported assignment errors. + * Includes remediation guidance for token and Copilot access setup. + * @param {string} assignmentErrors + * @returns {string} + */ +function buildAssignmentErrorsContext(assignmentErrors) { + if (!assignmentErrors || !assignmentErrors.trim()) { + return ""; + } + + let context = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues or pull requests."); + context += "\n**Assignment Errors:**\n"; + + const errorLines = assignmentErrors.split("\n").filter(line => line.trim()); + for (const errorLine of errorLines) { + const parts = errorLine.split(":"); + if (parts.length >= 4) { + const type = parts[0]; // "issue" or "pr" + const number = parts[1]; + const agent = parts[2]; + const error = parts.slice(3).join(":"); + context += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`; + } + } + + context += "\nTo resolve this, verify the agent token and Copilot access configuration:\n"; + context += "- Configure a valid `GH_AW_AGENT_TOKEN` with `issues: write` and `pull-requests: write` plus active Copilot entitlement\n"; + context += "- If your org supports it, add `permissions: { copilot-requests: write }` to use org inference without a personal token\n"; + context += "- Docs: https://github.github.com/gh-aw/reference/engines/#github-copilot-default\n\n"; + + return context; +} /** * Build a context string when assigning the Copilot coding agent to created issues failed. * @param {boolean} hasAssignCopilotFailures - Whether any copilot assignments failed @@ -2699,8 +2733,10 @@ async function main() { // in the engine output and sets the agentic_engine_timeout output. const isTimedOut = agentConclusion === "timed_out" || agenticEngineTimeout; - // Check if there are assignment errors (regardless of agent job status) - const hasAssignmentErrors = parseInt(assignmentErrorCount, 10) > 0; + // Check if there are assignment errors (regardless of agent job status). + // Use assignment_errors as the single source of truth because it includes + // both hard failures and skipped(ignore-if-error) assignment errors. + const hasAssignmentErrors = assignmentErrors.split("\n").some(line => line.trim()); // Check if there are copilot assignment failures for created issues (regardless of agent job status) const hasAssignCopilotFailures = parseInt(assignCopilotFailureCount, 10) > 0; @@ -3061,22 +3097,7 @@ async function main() { const runId = extractRunId(runUrl); // Build assignment errors context - let assignmentErrorsContext = ""; - if (hasAssignmentErrors && assignmentErrors) { - assignmentErrorsContext = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues due to insufficient permissions or missing token.") + "\n**Assignment Errors:**\n"; - const errorLines = assignmentErrors.split("\n").filter(line => line.trim()); - for (const errorLine of errorLines) { - const parts = errorLine.split(":"); - if (parts.length >= 4) { - const type = parts[0]; // "issue" or "pr" - const number = parts[1]; - const agent = parts[2]; - const error = parts.slice(3).join(":"); // Rest is the error message - assignmentErrorsContext += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`; - } - } - assignmentErrorsContext += "\n"; - } + const assignmentErrorsContext = buildAssignmentErrorsContext(assignmentErrors); // Build create_discussion errors context const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : ""; @@ -3284,22 +3305,7 @@ async function main() { const issueTemplate = fs.readFileSync(issueTemplatePath, "utf8"); // Build assignment errors context - let assignmentErrorsContext = ""; - if (hasAssignmentErrors && assignmentErrors) { - assignmentErrorsContext = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues due to insufficient permissions or missing token.") + "\n**Assignment Errors:**\n"; - const errorLines = assignmentErrors.split("\n").filter(line => line.trim()); - for (const errorLine of errorLines) { - const parts = errorLine.split(":"); - if (parts.length >= 4) { - const type = parts[0]; // "issue" or "pr" - const number = parts[1]; - const agent = parts[2]; - const error = parts.slice(3).join(":"); // Rest is the error message - assignmentErrorsContext += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`; - } - } - assignmentErrorsContext += "\n"; - } + const assignmentErrorsContext = buildAssignmentErrorsContext(assignmentErrors); // Build create_discussion errors context const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : ""; @@ -3531,6 +3537,7 @@ module.exports = { loadToolDenialsExceededEvents, buildToolDenialsExceededContext, buildCredentialAuthErrorContext, + buildAssignmentErrorsContext, buildAICreditsRateLimitErrorContext, buildUnknownModelAICreditsContext, hasEngineMaxRunsExceededSignal, diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 372b10297e5..1278d212261 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -12,6 +12,7 @@ describe("handle_agent_failure", () => { let buildReportIncompleteContext; let buildFailureIssueTitle; let buildSecretVerificationContext; + let buildAssignmentErrorsContext; let getActionFailureIssueExpiresHours; const ENGINE_RATE_LIMIT_TEMPLATE = "> [!WARNING]\n> **Engine Rate Limited (HTTP 429)**\n> OTLP telemetry\n> {engine_label}\n"; const ENGINE_MAX_RUNS_EXCEEDED_TEMPLATE = "> [!WARNING]\n> **Engine Max Runs Exceeded**\n> max-runs guardrail\n> {engine_label}\n"; @@ -31,7 +32,16 @@ describe("handle_agent_failure", () => { // Reset module registry so each test gets a fresh require vi.resetModules(); - ({ main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildReportIncompleteContext, buildFailureIssueTitle, buildSecretVerificationContext, getActionFailureIssueExpiresHours } = require("./handle_agent_failure.cjs")); + ({ + main, + buildCodePushFailureContext, + buildPushRepoMemoryFailureContext, + buildReportIncompleteContext, + buildFailureIssueTitle, + buildSecretVerificationContext, + buildAssignmentErrorsContext, + getActionFailureIssueExpiresHours, + } = require("./handle_agent_failure.cjs")); }); afterEach(() => { @@ -1311,6 +1321,29 @@ describe("handle_agent_failure", () => { expect(buildSecretVerificationContext("", "")).toBe(""); }); + describe("buildAssignmentErrorsContext", () => { + it("returns empty string when there are no assignment errors", () => { + expect(buildAssignmentErrorsContext("")).toBe(""); + }); + + it("returns empty string for whitespace-only input", () => { + expect(buildAssignmentErrorsContext(" ")).toBe(""); + expect(buildAssignmentErrorsContext("\n")).toBe(""); + expect(buildAssignmentErrorsContext("\n\n")).toBe(""); + }); + + it("renders assignment failures with token guidance docs", () => { + const result = buildAssignmentErrorsContext("issue:42:copilot:Bad credentials\npr:7:copilot:copilot coding agent is not available for this repository"); + + expect(result).toContain("Agent Assignment Failed"); + expect(result).toContain("Issue #42 (agent: copilot): Bad credentials"); + expect(result).toContain("PR #7 (agent: copilot): copilot coding agent is not available for this repository"); + expect(result).toContain("GH_AW_AGENT_TOKEN"); + expect(result).toContain("copilot-requests: write"); + expect(result).toContain("https://github.github.com/gh-aw/reference/engines/#github-copilot-default"); + }); + }); + it("returns generic warning for non-copilot engines when verification failed", () => { const result = buildSecretVerificationContext("failed", "claude"); expect(result).toContain("Secret Verification Failed");