diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 402a1811e57..a0756435e97 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -72,34 +72,86 @@ function getAgentName(assignee) { /** * Return list of coding agent bot login names that are currently available as assignable actors - * in this repository, as determined by checkUserCanBeAssigned. + * in this repository, preferring issue-scoped checks when issue/PR context is available + * and falling back to repository-scoped checks. * @param {string} owner * @param {string} repo + * @param {number|string|null} [issueNumber] * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) * @returns {Promise} */ -async function getAvailableAgentLogins(owner, repo, githubClient = github) { +async function getAvailableAgentLogins(owner, repo, issueNumber = null, githubClient = github) { // Deduplicate defensively so future alias additions across agents do not duplicate REST lookups. const knownValues = [...new Set(Object.values(AGENT_LOGIN_NAMES).flat())]; const available = []; for (const login of knownValues) { try { - await githubClient.rest.issues.checkUserCanBeAssigned({ - owner, - repo, - assignee: login, - }); + await validateAssigneeAlias(owner, repo, login, issueNumber, githubClient); available.push(login); } catch (e) { const status = e && typeof e === "object" && "status" in e ? e.status : undefined; if (status !== 404) { - core.debug(`Failed to check assignability for ${login}: ${getErrorMessage(e)}`); + core.info(`Failed to check assignability for ${login}: ${getErrorMessage(e)}`); } } } return available.sort(); } +/** + * Validate whether an assignee alias can be assigned in the repository context. + * Prefer issue-level assignability checks when issue/PR number is available because + * some agent bots are not surfaced by repository-scoped checks. + * @param {string} owner + * @param {string} repo + * @param {string} assignee + * @param {number|string|null} issueNumber + * @param {Object} githubClient + */ +async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubClient) { + const parsedIssueNumber = Number(issueNumber); + const hasValidIssueNumber = Number.isInteger(parsedIssueNumber) && parsedIssueNumber > 0; + const hasIssueScopedRequest = typeof githubClient?.request === "function"; + + if (issueNumber && hasValidIssueNumber && hasIssueScopedRequest) { + core.info(`Checking assignee alias ${assignee} via issue-scoped endpoint for ${owner}/${repo}#${parsedIssueNumber}`); + try { + const issueScopedResponse = await githubClient.request("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + owner, + repo, + issue_number: parsedIssueNumber, + assignee, + }); + const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; + if (issueScopedStatus !== undefined && Number.isInteger(issueScopedStatus) && issueScopedStatus >= 200 && issueScopedStatus < 300) { + core.info(`Assignee alias ${assignee} is assignable via issue-scoped check`); + return; + } + core.info(`Issue-scoped assignee check returned unexpected response for ${assignee} (status ${issueScopedStatus ?? "unknown"}); falling back to repository-scoped check`); + } catch (e) { + const status = e && typeof e === "object" && "status" in e ? e.status : undefined; + // Some coding-agent bot aliases can return 404 on issue-scoped checks even when + // assignment may still succeed; use repository-scoped endpoint as fallback. + if (status !== 404 && status !== 422) { + core.info(`Issue-scoped assignee check failed for ${assignee} with status ${status ?? "unknown"}: ${getErrorMessage(e)}`); + throw e; + } + core.info(`Issue-scoped assignee check returned ${status} for ${assignee}; falling back to repository-scoped check`); + } + } else if (issueNumber && !hasValidIssueNumber) { + core.info(`Skipping issue-scoped assignee check for ${assignee}: invalid issue number ${String(issueNumber)}`); + } else if (issueNumber && !hasIssueScopedRequest) { + core.info(`Skipping issue-scoped assignee check for ${assignee}: github client does not support request()`); + } + core.info(`Checking assignee alias ${assignee} via repository-scoped endpoint for ${owner}/${repo}`); + await githubClient.rest.issues.checkUserCanBeAssigned({ + owner, + repo, + assignee, + }); + core.info(`Assignee alias ${assignee} is assignable via repository-scoped check`); +} + /** * Return assignable bot logins from the repository assignee list. * @param {string} owner @@ -145,10 +197,11 @@ async function getAssignableBots(owner, repo, githubClient = github) { * @param {string} owner - Repository owner * @param {string} repo - Repository name * @param {string} agentName - Agent name (copilot) + * @param {number|string|null} [issueNumber] - Optional issue/PR number for issue-scoped assignability check * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) * @returns {Promise} Agent ID or null if not found */ -async function findAgent(owner, repo, agentName, githubClient = github) { +async function findAgent(owner, repo, agentName, issueNumber = null, githubClient = github) { const loginNames = getAgentLogins(agentName); if (loginNames.length === 0) { core.error(`Unknown agent: ${agentName}. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`); @@ -161,11 +214,7 @@ async function findAgent(owner, repo, agentName, githubClient = github) { for (const loginName of loginNames) { try { core.info(`Checking assignee alias: ${loginName}`); - await githubClient.rest.issues.checkUserCanBeAssigned({ - owner, - repo, - assignee: loginName, - }); + await validateAssigneeAlias(owner, repo, loginName, issueNumber, githubClient); } catch (checkError) { const errorMessage = getErrorMessage(checkError); const status = checkError?.status; @@ -536,7 +585,7 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) { try { // Find agent using the github object authenticated via step-level github-token core.info(`Looking for ${agentName} coding agent...`); - const agentId = await findAgent(owner, repo, agentName); + const agentId = await findAgent(owner, repo, agentName, issueNumber); if (!agentId) { return { success: false, error: `${agentName} coding agent is not available for this repository` }; } diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index 5de434d4849..ca0229ef42a 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -118,7 +118,38 @@ describe("assign_agent_helpers.cjs", () => { const result = await getAvailableAgentLogins("owner", "repo"); expect(result).toEqual([]); - expect(mockCore.debug).toHaveBeenCalledWith(expect.stringContaining("Failed to check assignability for copilot-swe-agent")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Failed to check assignability for copilot-swe-agent")); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledTimes(5); + }); + + it("should use issue-scoped assignee checks when issue number is provided", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.request.mockRejectedValueOnce(err404).mockResolvedValueOnce({ status: 204 }).mockRejectedValue(err404); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(err404); + + const result = await getAvailableAgentLogins("owner", "repo", 123); + + expect(result).toEqual(["github-copilot-enterprise"]); + expect(mockGithub.request).toHaveBeenCalledWith("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + owner: "owner", + repo: "repo", + issue_number: 123, + assignee: "copilot-swe-agent", + }); + expect(mockGithub.request).toHaveBeenCalledTimes(5); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledTimes(4); + }); + + it("should fall back to repository-scoped checks when issue-scoped check returns 422", async () => { + const err422 = Object.assign(new Error("Validation Failed"), { status: 422 }); + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.request.mockRejectedValue(err422); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404).mockResolvedValueOnce({}).mockRejectedValue(err404); + + const result = await getAvailableAgentLogins("owner", "repo", 123); + + expect(result).toEqual(["github-copilot-enterprise"]); + expect(mockGithub.request).toHaveBeenCalledTimes(5); expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledTimes(5); }); }); @@ -210,6 +241,61 @@ describe("assign_agent_helpers.cjs", () => { expect(result).toBeNull(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Assignee alias copilot-swe-agent was not assignable")); }); + + it("should prefer issue-scoped assignee checks when issue number is provided", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.request.mockRejectedValueOnce(err404).mockResolvedValueOnce({ status: 204 }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 12345 } }); + + const result = await findAgent("owner", "repo", "copilot", 321); + + expect(result).toBe("12345"); + expect(mockGithub.request).toHaveBeenCalledWith("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + owner: "owner", + repo: "repo", + issue_number: 321, + assignee: "copilot-swe-agent", + }); + expect(mockGithub.request).toHaveBeenCalledWith("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + owner: "owner", + repo: "repo", + issue_number: 321, + assignee: "github-copilot-enterprise", + }); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledTimes(1); + }); + + it("should fall back to repository-scoped checks when issue number is invalid", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(err404); + + const result = await findAgent("owner", "repo", "copilot", "not-a-number"); + + expect(result).toBeNull(); + expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalled(); + }); + + it("should fall back to repository-scoped checks when github client has no request method", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + const clientWithoutRequest = { + rest: { + issues: { + checkUserCanBeAssigned: vi.fn().mockRejectedValue(err404), + listAssignees: vi.fn().mockResolvedValue({ data: [] }), + }, + users: { + getByUsername: vi.fn(), + }, + }, + }; + + const result = await findAgent("owner", "repo", "copilot", 123, clientWithoutRequest); + + expect(result).toBeNull(); + expect(clientWithoutRequest.rest.issues.checkUserCanBeAssigned).toHaveBeenCalled(); + }); }); describe("getIssueDetails", () => { @@ -400,8 +486,8 @@ describe("assign_agent_helpers.cjs", () => { describe("assignAgentToIssueByName", () => { it("should successfully assign copilot agent", async () => { - // findAgent: checkUserCanBeAssigned + getByUsername - mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + // findAgent: issue-scoped assignee check + getByUsername + mockGithub.request.mockResolvedValueOnce({ status: 204 }); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -427,6 +513,7 @@ describe("assign_agent_helpers.cjs", () => { it("should return error when agent is not available", async () => { const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.request.mockRejectedValue(err404); mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(err404); mockGithub.rest.issues.listAssignees.mockResolvedValue({ data: [] }); @@ -438,7 +525,7 @@ describe("assign_agent_helpers.cjs", () => { it("should report already assigned when agent is in assignees", async () => { // findAgent - mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.request.mockResolvedValueOnce({ status: 204 }); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails - agent already assigned mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -460,7 +547,7 @@ describe("assign_agent_helpers.cjs", () => { it("should skip assignment when a secondary copilot alias is already assigned", async () => { // findAgent resolves via primary alias with id 999 - mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.request.mockResolvedValueOnce({ status: 204 }); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails - a secondary alias is the current assignee (different id, same agent) mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -477,7 +564,7 @@ describe("assign_agent_helpers.cjs", () => { const result = await assignAgentToIssueByName("owner", "repo", 123, "copilot"); expect(result.success).toBe(true); - expect(mockGithub.request).not.toHaveBeenCalled(); + expect(mockGithub.request).toHaveBeenCalledTimes(1); expect(mockCore.info).toHaveBeenCalledWith("copilot is already assigned to issue #123"); }); }); diff --git a/actions/setup/js/assign_copilot_to_created_issues.cjs b/actions/setup/js/assign_copilot_to_created_issues.cjs index 4c900bad6af..17b64980f1d 100644 --- a/actions/setup/js/assign_copilot_to_created_issues.cjs +++ b/actions/setup/js/assign_copilot_to_created_issues.cjs @@ -73,7 +73,7 @@ async function main() { // Find agent (reuse cached ID for same repo) if (!agentId) { core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(owner, repo, agentName); + agentId = await findAgent(owner, repo, agentName, issueNumber); if (!agentId) { throw new Error(`${ERR_PERMISSION}: ${agentName} coding agent is not available for this repository`); } diff --git a/actions/setup/js/assign_copilot_to_created_issues.test.cjs b/actions/setup/js/assign_copilot_to_created_issues.test.cjs index aaca4bdb757..937f784685b 100644 --- a/actions/setup/js/assign_copilot_to_created_issues.test.cjs +++ b/actions/setup/js/assign_copilot_to_created_issues.test.cjs @@ -82,7 +82,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const repo = repoParts[1]; if (!agentId) { - agentId = await findAgent(owner, repo, agentName); + agentId = await findAgent(owner, repo, agentName, issueNumber); } const issueDetails = await getIssueDetails(owner, repo, issueNumber); @@ -109,7 +109,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const repo = repoParts[1]; if (!agentId) { - agentId = await findAgent(owner, repo, agentName); + agentId = await findAgent(owner, repo, agentName, issueNumber); } const issueDetails = await getIssueDetails(owner, repo, issueNumber); @@ -117,7 +117,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { } })()`); - expect(findAgent).toHaveBeenCalledWith("owner", "repo", "copilot"); + expect(findAgent).toHaveBeenCalledWith("owner", "repo", "copilot", 123); expect(getIssueDetails).toHaveBeenCalledWith("owner", "repo", 123); expect(assignAgentToIssue).toHaveBeenCalledWith("ISSUE_123", "AGENT_456", [], "copilot"); }); @@ -150,7 +150,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const repo = repoParts[1]; if (!agentId) { - agentId = await findAgent(owner, repo, agentName); + agentId = await findAgent(owner, repo, agentName, issueNumber); } const issueDetails = await getIssueDetails(owner, repo, issueNumber); @@ -216,7 +216,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const owner = repoParts[0]; const repo = repoParts[1]; - const agentId = await findAgent(owner, repo, agentName); + const agentId = await findAgent(owner, repo, agentName, issueNumber); if (!agentId) { throw new Error(\`\${agentName} coding agent is not available for this repository\`); } @@ -251,7 +251,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const owner = repoParts[0]; const repo = repoParts[1]; - const agentId = await findAgent(owner, repo, agentName); + const agentId = await findAgent(owner, repo, agentName, issueNumber); const issueDetails = await getIssueDetails(owner, repo, issueNumber); if (issueDetails.currentAssignees.includes(agentId)) { @@ -292,7 +292,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const owner = repoParts[0]; const repo = repoParts[1]; - const agentId = await findAgent(owner, repo, agentName); + const agentId = await findAgent(owner, repo, agentName, issueNumber); const issueDetails = await getIssueDetails(owner, repo, issueNumber); const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName); @@ -324,7 +324,7 @@ describe("assign_copilot_to_created_issues.cjs", () => { const owner = repoParts[0]; const repo = repoParts[1]; - const agentId = await findAgent(owner, repo, agentName); + const agentId = await findAgent(owner, repo, agentName, issueNumber); await getIssueDetails(owner, repo, issueNumber); } })()`); diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 295b89b2339..9263ec31db2 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -330,7 +330,7 @@ async function main(config = {}) { let agentId = agentCache[agentName]; if (!agentId) { core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(effectiveOwner, effectiveRepo, agentName, githubClient); + agentId = await findAgent(effectiveOwner, effectiveRepo, agentName, issueNumber || pullNumber, githubClient); if (!agentId) { throw new Error(`${agentName} coding agent is not available for this repository`); } @@ -405,7 +405,7 @@ async function main(config = {}) { if (isAvailabilityError) { try { - const available = await getAvailableAgentLogins(effectiveOwner, effectiveRepo, githubClient); + const available = await getAvailableAgentLogins(effectiveOwner, effectiveRepo, issueNumber || pullNumber, githubClient); if (available.length > 0) errorMessage += ` (available agents: ${available.join(", ")})`; } catch (e) { core.debug("Failed to enrich unavailable agent message with available list"); diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index fffbe254250..d2ae930db8f 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -485,9 +485,10 @@ describe("assign_to_agent", () => { expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #6587")); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned copilot coding agent to issue #6587")); - expect(mockGithub.request).toHaveBeenCalledTimes(2); - expect(mockGithub.request.mock.calls[0][1]).toMatchObject({ owner: "test-owner", repo: "ios-repo" }); - expect(mockGithub.request.mock.calls[1][1]).toMatchObject({ owner: "test-owner", repo: "android-repo" }); + const assignmentCalls = mockGithub.request.mock.calls.filter(([route]) => route === "POST /agents/repos/{owner}/{repo}/tasks"); + expect(assignmentCalls).toHaveLength(2); + expect(assignmentCalls[0][1]).toMatchObject({ owner: "test-owner", repo: "ios-repo" }); + expect(assignmentCalls[1][1]).toMatchObject({ owner: "test-owner", repo: "android-repo" }); expect(mockSleep).toHaveBeenCalledTimes(1); expect(mockSleep).toHaveBeenCalledWith(10000); @@ -542,7 +543,8 @@ describe("assign_to_agent", () => { await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #6587")); - expect(mockGithub.request).toHaveBeenCalledTimes(1); + const assignmentCalls = mockGithub.request.mock.calls.filter(([route]) => route === "POST /agents/repos/{owner}/{repo}/tasks"); + expect(assignmentCalls).toHaveLength(1); expect(mockSleep).toHaveBeenCalledTimes(1); expect(mockSleep).toHaveBeenCalledWith(10000); }); @@ -571,7 +573,8 @@ describe("assign_to_agent", () => { await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); - expect(mockGithub.request).not.toHaveBeenCalled(); + const assignmentCalls = mockGithub.request.mock.calls.filter(([route]) => route === "POST /agents/repos/{owner}/{repo}/tasks"); + expect(assignmentCalls).toHaveLength(0); }); it("should still skip when agent is already assigned with global pull-request-repo but no per-item override", async () => { @@ -604,7 +607,8 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); // Should NOT have called the assignment mutation (only 3 GraphQL calls: repo lookup, find agent, get issue) expect(mockGithub.rest.repos.get).toHaveBeenCalledTimes(1); // global PR repo lookup - expect(mockGithub.request).not.toHaveBeenCalled(); // no assignment since already assigned + const assignmentCalls = mockGithub.request.mock.calls.filter(([route]) => route === "POST /agents/repos/{owner}/{repo}/tasks"); + expect(assignmentCalls).toHaveLength(0); // no assignment since already assigned expect(mockCore.setFailed).not.toHaveBeenCalled(); }); @@ -648,13 +652,18 @@ describe("assign_to_agent", () => { data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, }); // Assignment fails with 502 - mockGithub.request.mockRejectedValueOnce({ - response: { - status: 502, - url: "https://api.github.com/repos/test-owner/test-repo/tasks", - headers: { "content-type": "text/html" }, - data: "\n502 Bad Gateway\n\n

502 Bad Gateway

\n
nginx
\n\n\n", - }, + mockGithub.request.mockImplementation(route => { + if (route === "POST /agents/repos/{owner}/{repo}/tasks") { + return Promise.reject({ + response: { + status: 502, + url: "https://api.github.com/repos/test-owner/test-repo/tasks", + headers: { "content-type": "text/html" }, + data: "\n502 Bad Gateway\n\n

502 Bad Gateway

\n
nginx
\n\n\n", + }, + }); + } + return Promise.resolve({ data: { id: "task-123" } }); }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -687,7 +696,12 @@ describe("assign_to_agent", () => { data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, }); // Assignment fails with 502 message - mockGithub.request.mockRejectedValueOnce(new Error("502 Bad Gateway")); + mockGithub.request.mockImplementation(route => { + if (route === "POST /agents/repos/{owner}/{repo}/tasks") { + return Promise.reject(new Error("502 Bad Gateway")); + } + return Promise.resolve({ data: { id: "task-123" } }); + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1157,7 +1171,12 @@ describe("assign_to_agent", () => { // Simulate a different error (not auth-related) during assignment const otherError = new Error("Network timeout"); - mockGithub.request.mockRejectedValue(otherError); + mockGithub.request.mockImplementation(route => { + if (route === "POST /agents/repos/{owner}/{repo}/tasks") { + return Promise.reject(otherError); + } + return Promise.resolve({ data: { id: "task-123" } }); + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index ac6626f4702..6047b77fbfa 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -1073,7 +1073,7 @@ async function main(config = {}) { } core.info(`Assigning copilot coding agent to issue #${issue.number} in ${qualifiedItemRepo}...`); try { - const agentId = await findAgent(repoParts.owner, repoParts.repo, "copilot", copilotClient); + const agentId = await findAgent(repoParts.owner, repoParts.repo, "copilot", issue.number, copilotClient); if (!agentId) { core.warning(`copilot coding agent is not available for ${qualifiedItemRepo}`); } else { diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 5e11fd28143..aba54d0c44d 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -676,7 +676,7 @@ async function main(config = {}) { } core.info(`Assigning copilot coding agent to fallback issue #${issueNumber} in ${owner}/${repo}...`); try { - const agentId = await findAgent(owner, repo, "copilot", copilotClient); + const agentId = await findAgent(owner, repo, "copilot", issueNumber, copilotClient); if (!agentId) { core.warning(`copilot coding agent is not available for ${owner}/${repo}`); return; diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 13a774912b9..653744838f4 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3296,8 +3296,10 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { expect(issueCall.assignees).not.toContain("copilot"); expect(issueCall.assignees).toContain("user1"); - // REST task creation should be called once for copilot assignment - expect(global.github.request).toHaveBeenCalledTimes(1); + // One request for issue-scoped alias validation and one for REST task creation + expect(global.github.request).toHaveBeenCalledTimes(2); + const requestedRoutes = global.github.request.mock.calls.map(([route]) => route); + expect(requestedRoutes).toEqual(expect.arrayContaining(["GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", "POST /agents/repos/{owner}/{repo}/tasks"])); }); it("should use configured fallback_labels for fallback issues instead of PR labels", async () => { diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index 341a9c4ae78..bf76aad5905 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -329,6 +329,8 @@ function isCodexJsonlFormat(lines) { * @returns {{markdown: string, logEntries: Array, mcpFailures: Array, maxTurnsHit: boolean}} Parsed log data */ function parseCodexJsonl(logContent) { + const DEFAULT_STATUS_ICON = "🔧"; + const lines = logContent.split("\n"); const parsedData = []; let usage = null; @@ -423,10 +425,11 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const [server, toolName] = item.toolName.split("__"); - markdown += formatCodexToolCall(server, toolName, item.params, item.response, item.statusIcon); + const toolNameValue = item.toolName || "unknown-server__unknown-tool"; + const [server, toolName] = toolNameValue.split("__", 2); + markdown += formatCodexToolCall(server, toolName, item.params || "", item.response || "", item.statusIcon || DEFAULT_STATUS_ICON); } else if (item.type === "bash") { - markdown += formatCodexBashCall(item.content, item.response, item.statusIcon); + markdown += formatCodexBashCall(item.content || "", item.response || "", item.statusIcon || DEFAULT_STATUS_ICON); } } markdown += "\n## 📊 Information\n\n";