Skip to content

Commit 47e29c6

Browse files
authored
Surface assign-to-agent auth/availability failures in agent failure issues/comments (#41336)
1 parent 7088d7a commit 47e29c6

4 files changed

Lines changed: 103 additions & 45 deletions

File tree

actions/setup/js/assign_to_agent.cjs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,17 @@ async function main(config = {}) {
399399
const errorType = isAuthError ? "authentication/permission" : "agent availability";
400400
core.warning(`Agent assignment failed for ${agentName} on ${type} #${number} due to ${errorType} error. Skipping due to ignore-if-error=true.`);
401401
core.info(`Error details: ${errorMessage}`);
402-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true, skipped: true });
402+
_allResults.push({
403+
issue_number: issueNumber,
404+
pull_number: pullNumber,
405+
agent: agentName,
406+
owner: effectiveOwner,
407+
repo: effectiveRepo,
408+
pull_request_repo: effectivePullRequestRepoSlug,
409+
success: true,
410+
skipped: true,
411+
error: errorMessage,
412+
});
403413
return { success: true, skipped: true };
404414
}
405415

@@ -451,18 +461,24 @@ function getAssignToAgentAssigned() {
451461

452462
/**
453463
* Returns the "assignment_errors" output string for step outputs.
454-
* Format: "issue:N:agent:error" or "pr:N:agent:error" per failure, newline-separated.
464+
* Format: "issue:N:agent:error" or "pr:N:agent:error" per failure/skipped-with-error,
465+
* newline-separated.
455466
* @returns {string}
456467
*/
457468
function getAssignToAgentErrors() {
458-
return _allResults
459-
.filter(r => !r.success && !r.skipped)
460-
.map(r => {
461-
const number = r.issue_number || r.pull_number;
462-
const prefix = r.issue_number ? "issue" : "pr";
463-
return `${prefix}:${number}:${r.agent}:${r.error}`;
464-
})
465-
.join("\n");
469+
return (
470+
_allResults
471+
// Include skipped(ignore-if-error) entries that still captured an error so
472+
// downstream failure handling can surface assignment problems in issue/comment reports.
473+
// Include hard failures (!success) and ignored failures (skipped=true with error).
474+
.filter(r => r.error && (r.skipped || !r.success))
475+
.map(r => {
476+
const number = r.issue_number || r.pull_number;
477+
const prefix = r.issue_number ? "issue" : "pr";
478+
return `${prefix}:${number}:${r.agent}:${r.error}`;
479+
})
480+
.join("\n")
481+
);
466482
}
467483

468484
/**

actions/setup/js/assign_to_agent.test.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,8 @@ describe("assign_to_agent", () => {
10541054

10551055
// Should not fail the workflow
10561056
expect(mockCore.setFailed).not.toHaveBeenCalled();
1057+
expect(mockCore.setOutput).toHaveBeenCalledWith("assignment_error_count", "0");
1058+
expect(mockCore.setOutput).toHaveBeenCalledWith("assignment_errors", expect.stringContaining("Bad credentials"));
10571059

10581060
// Summary should show skipped assignments
10591061
expect(mockCore.summary.addRaw).toHaveBeenCalled();

actions/setup/js/handle_agent_failure.cjs

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,6 +2030,40 @@ function buildCredentialAuthErrorContext(auditJsonlPathOverride) {
20302030
const templatePath = getPromptPath("credential_auth_error.md");
20312031
return "\n" + renderTemplateFromFile(templatePath, { providers: providersList });
20322032
}
2033+
2034+
/**
2035+
* Build a context string when assign_to_agent reported assignment errors.
2036+
* Includes remediation guidance for token and Copilot access setup.
2037+
* @param {string} assignmentErrors
2038+
* @returns {string}
2039+
*/
2040+
function buildAssignmentErrorsContext(assignmentErrors) {
2041+
if (!assignmentErrors || !assignmentErrors.trim()) {
2042+
return "";
2043+
}
2044+
2045+
let context = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues or pull requests.");
2046+
context += "\n**Assignment Errors:**\n";
2047+
2048+
const errorLines = assignmentErrors.split("\n").filter(line => line.trim());
2049+
for (const errorLine of errorLines) {
2050+
const parts = errorLine.split(":");
2051+
if (parts.length >= 4) {
2052+
const type = parts[0]; // "issue" or "pr"
2053+
const number = parts[1];
2054+
const agent = parts[2];
2055+
const error = parts.slice(3).join(":");
2056+
context += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`;
2057+
}
2058+
}
2059+
2060+
context += "\nTo resolve this, verify the agent token and Copilot access configuration:\n";
2061+
context += "- Configure a valid `GH_AW_AGENT_TOKEN` with `issues: write` and `pull-requests: write` plus active Copilot entitlement\n";
2062+
context += "- If your org supports it, add `permissions: { copilot-requests: write }` to use org inference without a personal token\n";
2063+
context += "- Docs: https://github.github.com/gh-aw/reference/engines/#github-copilot-default\n\n";
2064+
2065+
return context;
2066+
}
20332067
/**
20342068
* Build a context string when assigning the Copilot coding agent to created issues failed.
20352069
* @param {boolean} hasAssignCopilotFailures - Whether any copilot assignments failed
@@ -2699,8 +2733,10 @@ async function main() {
26992733
// in the engine output and sets the agentic_engine_timeout output.
27002734
const isTimedOut = agentConclusion === "timed_out" || agenticEngineTimeout;
27012735

2702-
// Check if there are assignment errors (regardless of agent job status)
2703-
const hasAssignmentErrors = parseInt(assignmentErrorCount, 10) > 0;
2736+
// Check if there are assignment errors (regardless of agent job status).
2737+
// Use assignment_errors as the single source of truth because it includes
2738+
// both hard failures and skipped(ignore-if-error) assignment errors.
2739+
const hasAssignmentErrors = assignmentErrors.split("\n").some(line => line.trim());
27042740

27052741
// Check if there are copilot assignment failures for created issues (regardless of agent job status)
27062742
const hasAssignCopilotFailures = parseInt(assignCopilotFailureCount, 10) > 0;
@@ -3061,22 +3097,7 @@ async function main() {
30613097
const runId = extractRunId(runUrl);
30623098

30633099
// Build assignment errors context
3064-
let assignmentErrorsContext = "";
3065-
if (hasAssignmentErrors && assignmentErrors) {
3066-
assignmentErrorsContext = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues due to insufficient permissions or missing token.") + "\n**Assignment Errors:**\n";
3067-
const errorLines = assignmentErrors.split("\n").filter(line => line.trim());
3068-
for (const errorLine of errorLines) {
3069-
const parts = errorLine.split(":");
3070-
if (parts.length >= 4) {
3071-
const type = parts[0]; // "issue" or "pr"
3072-
const number = parts[1];
3073-
const agent = parts[2];
3074-
const error = parts.slice(3).join(":"); // Rest is the error message
3075-
assignmentErrorsContext += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`;
3076-
}
3077-
}
3078-
assignmentErrorsContext += "\n";
3079-
}
3100+
const assignmentErrorsContext = buildAssignmentErrorsContext(assignmentErrors);
30803101

30813102
// Build create_discussion errors context
30823103
const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : "";
@@ -3284,22 +3305,7 @@ async function main() {
32843305
const issueTemplate = fs.readFileSync(issueTemplatePath, "utf8");
32853306

32863307
// Build assignment errors context
3287-
let assignmentErrorsContext = "";
3288-
if (hasAssignmentErrors && assignmentErrors) {
3289-
assignmentErrorsContext = buildWarningAlertLine("Agent Assignment Failed", "Failed to assign agent to issues due to insufficient permissions or missing token.") + "\n**Assignment Errors:**\n";
3290-
const errorLines = assignmentErrors.split("\n").filter(line => line.trim());
3291-
for (const errorLine of errorLines) {
3292-
const parts = errorLine.split(":");
3293-
if (parts.length >= 4) {
3294-
const type = parts[0]; // "issue" or "pr"
3295-
const number = parts[1];
3296-
const agent = parts[2];
3297-
const error = parts.slice(3).join(":"); // Rest is the error message
3298-
assignmentErrorsContext += `- ${type === "issue" ? "Issue" : "PR"} #${number} (agent: ${agent}): ${error}\n`;
3299-
}
3300-
}
3301-
assignmentErrorsContext += "\n";
3302-
}
3308+
const assignmentErrorsContext = buildAssignmentErrorsContext(assignmentErrors);
33033309

33043310
// Build create_discussion errors context
33053311
const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : "";
@@ -3531,6 +3537,7 @@ module.exports = {
35313537
loadToolDenialsExceededEvents,
35323538
buildToolDenialsExceededContext,
35333539
buildCredentialAuthErrorContext,
3540+
buildAssignmentErrorsContext,
35343541
buildAICreditsRateLimitErrorContext,
35353542
buildUnknownModelAICreditsContext,
35363543
hasEngineMaxRunsExceededSignal,

actions/setup/js/handle_agent_failure.test.cjs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe("handle_agent_failure", () => {
1212
let buildReportIncompleteContext;
1313
let buildFailureIssueTitle;
1414
let buildSecretVerificationContext;
15+
let buildAssignmentErrorsContext;
1516
let getActionFailureIssueExpiresHours;
1617
const ENGINE_RATE_LIMIT_TEMPLATE = "> [!WARNING]\n> **Engine Rate Limited (HTTP 429)**\n> OTLP telemetry\n> {engine_label}\n";
1718
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", () => {
3132

3233
// Reset module registry so each test gets a fresh require
3334
vi.resetModules();
34-
({ main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildReportIncompleteContext, buildFailureIssueTitle, buildSecretVerificationContext, getActionFailureIssueExpiresHours } = require("./handle_agent_failure.cjs"));
35+
({
36+
main,
37+
buildCodePushFailureContext,
38+
buildPushRepoMemoryFailureContext,
39+
buildReportIncompleteContext,
40+
buildFailureIssueTitle,
41+
buildSecretVerificationContext,
42+
buildAssignmentErrorsContext,
43+
getActionFailureIssueExpiresHours,
44+
} = require("./handle_agent_failure.cjs"));
3545
});
3646

3747
afterEach(() => {
@@ -1311,6 +1321,29 @@ describe("handle_agent_failure", () => {
13111321
expect(buildSecretVerificationContext("", "")).toBe("");
13121322
});
13131323

1324+
describe("buildAssignmentErrorsContext", () => {
1325+
it("returns empty string when there are no assignment errors", () => {
1326+
expect(buildAssignmentErrorsContext("")).toBe("");
1327+
});
1328+
1329+
it("returns empty string for whitespace-only input", () => {
1330+
expect(buildAssignmentErrorsContext(" ")).toBe("");
1331+
expect(buildAssignmentErrorsContext("\n")).toBe("");
1332+
expect(buildAssignmentErrorsContext("\n\n")).toBe("");
1333+
});
1334+
1335+
it("renders assignment failures with token guidance docs", () => {
1336+
const result = buildAssignmentErrorsContext("issue:42:copilot:Bad credentials\npr:7:copilot:copilot coding agent is not available for this repository");
1337+
1338+
expect(result).toContain("Agent Assignment Failed");
1339+
expect(result).toContain("Issue #42 (agent: copilot): Bad credentials");
1340+
expect(result).toContain("PR #7 (agent: copilot): copilot coding agent is not available for this repository");
1341+
expect(result).toContain("GH_AW_AGENT_TOKEN");
1342+
expect(result).toContain("copilot-requests: write");
1343+
expect(result).toContain("https://github.github.com/gh-aw/reference/engines/#github-copilot-default");
1344+
});
1345+
});
1346+
13141347
it("returns generic warning for non-copilot engines when verification failed", () => {
13151348
const result = buildSecretVerificationContext("failed", "claude");
13161349
expect(result).toContain("Secret Verification Failed");

0 commit comments

Comments
 (0)