diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 2933768f67e..1bdd1dc7be6 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -660,17 +660,21 @@ function buildLockdownCheckFailedContext(hasLockdownCheckFailed) { /** * Build a context string when assigning the Copilot coding agent to created issues failed. + * Uses progressive disclosure via
sections to present actionable information. * @param {boolean} hasAssignCopilotFailures - Whether any copilot assignments failed * @param {string} assignCopilotErrors - Newline-separated list of "issue:number:copilot:error" entries + * @param {string} [runUrl] - URL of the current workflow run for the footer link * @returns {string} Formatted context string, or empty string if no failures */ -function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors) { +function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors, runUrl = "") { if (!hasAssignCopilotFailures) { return ""; } - // Build a list of failed issue assignments - let issueList = ""; + // Parse and categorize errors + const failedIssues = []; + let hasAvailabilityErrors = false; + let hasPermissionErrors = false; if (assignCopilotErrors) { const errorLines = assignCopilotErrors.split("\n").filter(line => line.trim()); for (const errorLine of errorLines) { @@ -678,13 +682,76 @@ function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilo if (parts.length >= 4) { const number = parts[1]; const error = parts.slice(3).join(":"); // Rest is the error message - issueList += `- Issue #${number}: ${error}\n`; + failedIssues.push({ number, error }); + if (error.includes("not available") || error.includes("ERR_PERMISSION")) { + hasAvailabilityErrors = true; + } + if (error.includes("Forbidden") || error.includes("Insufficient permissions") || error.includes("Resource not accessible")) { + hasPermissionErrors = true; + } } } } - const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/assign_copilot_to_created_issues_failure.md`; - return "\n" + renderTemplateFromFile(templatePath, { issues: issueList }); + const agentLogin = "copilot-swe-agent"; + const failureCount = failedIssues.length || 1; // Fallback to 1 if parsing produced no items + + let context = `\n**⚠️ Copilot Agent Assignment Failed**: Could not assign the Copilot coding agent to ${failureCount} issue(s).\n`; + + // Progressive disclosure: failed assignments list + if (failedIssues.length > 0) { + context += `\n
\n📋 ${failureCount} failed assignment(s)\n\n`; + for (const { number, error } of failedIssues) { + context += `- Issue #${number}: \`${error}\`\n`; + } + context += "\n
\n"; + } + + // Manual assignment instructions + context += `\n
\n🔧 Assign manually\n\nOpen each affected issue and assign \`@${agentLogin}\` as the assignee, or use the GitHub CLI:\n\n`; + context += "```bash\n"; + if (failedIssues.length > 0) { + for (const { number } of failedIssues) { + context += `gh issue edit ${number} --add-assignee ${agentLogin}\n`; + } + } else { + context += `gh issue edit --add-assignee ${agentLogin}\n`; + } + context += "```\n\n
\n"; + + // Error-specific guidance + context += "\n
\n💡 Possible causes\n\n"; + if (hasAvailabilityErrors) { + context += `**Copilot coding agent not available** (\`@${agentLogin}\` was not found as an assignable actor):\n\n`; + context += "- Copilot coding agent may not be enabled for this repository\n"; + context += "- The `GH_AW_AGENT_TOKEN` may belong to an account without a Copilot subscription\n\n"; + context += "Go to **Settings → Copilot** in your repository to enable the Copilot coding agent.\n\n"; + } + if (hasPermissionErrors) { + context += "**Permission error** — the token may lack required access:\n\n"; + context += "- Verify `GH_AW_AGENT_TOKEN` is set in repository secrets\n"; + context += "- The token must have `issues: write` permission\n"; + context += "- The token must belong to a user with an active Copilot subscription\n\n"; + context += '```bash\ngh aw secrets set GH_AW_AGENT_TOKEN --value "YOUR_TOKEN"\n```\n\n'; + } + if (!hasAvailabilityErrors && !hasPermissionErrors) { + context += "Common causes:\n\n"; + context += "- The `GH_AW_AGENT_TOKEN` secret is missing or expired\n"; + context += "- The Copilot coding agent is not enabled for this repository\n"; + context += "- The token lacks `issues: write` permission\n"; + context += "- GitHub API rate limiting\n\n"; + } + context += "See: [gh-aw authentication reference](https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx)\n\n"; + context += "
\n"; + + // Footer link to the workflow run + if (runUrl) { + const runIdMatch = runUrl.match(/\/actions\/runs\/(\d+)/); + const runId = runIdMatch ? runIdMatch[1] : ""; + context += `\n> [View workflow run${runId ? ` #${runId}` : ""}](${runUrl})\n`; + } + + return context; } /** @@ -1046,7 +1113,7 @@ async function main() { const lockdownCheckFailedContext = buildLockdownCheckFailedContext(hasLockdownCheckFailed); // Build copilot assignment failure context for created issues - const assignCopilotFailureContext = buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors); + const assignCopilotFailureContext = buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors, runUrl); // Create template context const templateContext = { @@ -1187,7 +1254,7 @@ async function main() { const lockdownCheckFailedContext = buildLockdownCheckFailedContext(hasLockdownCheckFailed); // Build copilot assignment failure context for created issues - const assignCopilotFailureContext = buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors); + const assignCopilotFailureContext = buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilotErrors, runUrl); // Create template context with sanitized workflow name const templateContext = { diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 9d241395045..c9a8105e923 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -8,6 +8,7 @@ const require = createRequire(import.meta.url); describe("handle_agent_failure", () => { let buildCodePushFailureContext; let buildPushRepoMemoryFailureContext; + let buildAssignCopilotFailureContext; beforeEach(() => { // Provide minimal GitHub Actions globals expected by require-time code @@ -24,7 +25,7 @@ describe("handle_agent_failure", () => { // Reset module registry so each test gets a fresh require vi.resetModules(); - ({ buildCodePushFailureContext, buildPushRepoMemoryFailureContext } = require("./handle_agent_failure.cjs")); + ({ buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAssignCopilotFailureContext } = require("./handle_agent_failure.cjs")); }); afterEach(() => { @@ -443,4 +444,108 @@ describe("handle_agent_failure", () => { expect(result).toContain("55"); }); }); + + // ────────────────────────────────────────────────────── + // buildAssignCopilotFailureContext + // ────────────────────────────────────────────────────── + + describe("buildAssignCopilotFailureContext", () => { + it("returns empty string when no failures", () => { + expect(buildAssignCopilotFailureContext(false, "")).toBe(""); + expect(buildAssignCopilotFailureContext(false, null)).toBe(""); + expect(buildAssignCopilotFailureContext(false, "issue:42:copilot:some error")).toBe(""); + }); + + it("returns formatted message when there are failures", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).toContain("Copilot Agent Assignment Failed"); + expect(result).toContain("#42"); + }); + + it("includes failed assignment details in a collapsible section", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).toContain("
"); + expect(result).toContain(""); + expect(result).toContain("failed assignment"); + expect(result).toContain("#42"); + expect(result).toContain("some error"); + }); + + it("includes manual assignment instructions with copilot-swe-agent login", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).toContain("copilot-swe-agent"); + expect(result).toContain("gh issue edit"); + expect(result).toContain("--add-assignee"); + }); + + it("includes possible causes section", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).toContain("Possible causes"); + expect(result).toContain("GH_AW_AGENT_TOKEN"); + }); + + it("shows availability error guidance when agent is not available", () => { + const errors = "issue:42:copilot:ERR_PERMISSION: copilot coding agent is not available for this repository"; + const result = buildAssignCopilotFailureContext(true, errors); + expect(result).toContain("not available"); + expect(result).toContain("Settings → Copilot"); + }); + + it("shows permission error guidance when token has insufficient permissions", () => { + const errors = "issue:42:copilot:Forbidden"; + const result = buildAssignCopilotFailureContext(true, errors); + expect(result).toContain("Permission error"); + expect(result).toContain("issues: write"); + expect(result).toContain("gh aw secrets set GH_AW_AGENT_TOKEN"); + }); + + it("shows permission error guidance for Resource not accessible error", () => { + const errors = "issue:42:copilot:Resource not accessible by integration"; + const result = buildAssignCopilotFailureContext(true, errors); + expect(result).toContain("Permission error"); + }); + + it("shows generic causes for unrecognized errors", () => { + const errors = "issue:42:copilot:ERR_API: Failed to get issue details"; + const result = buildAssignCopilotFailureContext(true, errors); + expect(result).toContain("Common causes"); + expect(result).toContain("rate limiting"); + }); + + it("includes a link to the authentication reference docs", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).toContain("gh-aw authentication reference"); + }); + + it("includes a footer link when runUrl is provided", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error", "https://github.com/owner/repo/actions/runs/12345678"); + expect(result).toContain("https://github.com/owner/repo/actions/runs/12345678"); + expect(result).toContain("#12345678"); + }); + + it("includes footer link without run ID when URL does not contain a run ID", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error", "https://example.com/run"); + expect(result).toContain("https://example.com/run"); + expect(result).toContain("View workflow run"); + }); + + it("omits footer link when runUrl is not provided", () => { + const result = buildAssignCopilotFailureContext(true, "issue:42:copilot:some error"); + expect(result).not.toContain("View workflow run"); + }); + + it("handles multiple failed issues", () => { + const errors = ["issue:42:copilot:some error", "issue:99:copilot:another error"].join("\n"); + const result = buildAssignCopilotFailureContext(true, errors); + expect(result).toContain("#42"); + expect(result).toContain("#99"); + expect(result).toContain("2 failed assignment"); + }); + + it("handles hasAssignCopilotFailures=true with empty errors string", () => { + const result = buildAssignCopilotFailureContext(true, ""); + expect(result).toContain("Copilot Agent Assignment Failed"); + expect(result).toContain("copilot-swe-agent"); + }); + }); }); diff --git a/actions/setup/md/assign_copilot_to_created_issues_failure.md b/actions/setup/md/assign_copilot_to_created_issues_failure.md deleted file mode 100644 index 0ad5453aaf1..00000000000 --- a/actions/setup/md/assign_copilot_to_created_issues_failure.md +++ /dev/null @@ -1,21 +0,0 @@ - -**🤖 Copilot Assignment Failed**: The workflow created an issue but could not assign the Copilot coding agent to it. This typically happens when: - -- The `GH_AW_AGENT_TOKEN` secret is missing or has expired -- The token does not have the `issues: write` permission -- The Copilot coding agent is not available for this repository -- GitHub API credentials are invalid (`Bad credentials`) - -**Failed assignments:** -{issues} - -To resolve this, verify that: -1. The `GH_AW_AGENT_TOKEN` secret is configured in your repository settings -2. The token belongs to an account with an active Copilot subscription -3. The token has `issues: write` permission for this repository - -```bash -gh aw secrets set GH_AW_AGENT_TOKEN --value "YOUR_TOKEN" -``` - -See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx