Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions actions/setup/js/assign_to_agent.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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")
);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions actions/setup/js/assign_to_agent.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
75 changes: 41 additions & 34 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
Comment thread
Copilot marked this conversation as resolved.

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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) : "";
Expand Down Expand Up @@ -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) : "";
Expand Down Expand Up @@ -3531,6 +3537,7 @@ module.exports = {
loadToolDenialsExceededEvents,
buildToolDenialsExceededContext,
buildCredentialAuthErrorContext,
buildAssignmentErrorsContext,
buildAICreditsRateLimitErrorContext,
buildUnknownModelAICreditsContext,
hasEngineMaxRunsExceededSignal,
Expand Down
35 changes: 34 additions & 1 deletion actions/setup/js/handle_agent_failure.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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");
Expand Down
Loading