From 75951929a084774aaf2acf75227052d4084e6f0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:46:01 +0000 Subject: [PATCH 01/12] Fix copilot assignee validation to use issue-scoped checks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 54 ++++++++++++---- .../setup/js/assign_agent_helpers.test.cjs | 64 +++++++++++++++++-- .../js/assign_copilot_to_created_issues.cjs | 2 +- .../assign_copilot_to_created_issues.test.cjs | 16 ++--- actions/setup/js/assign_to_agent.cjs | 2 +- actions/setup/js/create_issue.cjs | 2 +- actions/setup/js/create_pull_request.cjs | 2 +- 7 files changed, 112 insertions(+), 30 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 402a1811e57..3301749c4c0 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -75,20 +75,17 @@ function getAgentName(assignee) { * in this repository, as determined by checkUserCanBeAssigned. * @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; @@ -100,6 +97,40 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) { 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) { + if (issueNumber) { + try { + await githubClient.request("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + owner, + repo, + issue_number: Number(issueNumber), + assignee, + }); + return; + } catch (e) { + const status = e && typeof e === "object" && "status" in e ? e.status : undefined; + if (status !== 404 && status !== 422) { + throw e; + } + } + } + await githubClient.rest.issues.checkUserCanBeAssigned({ + owner, + repo, + assignee, + }); +} + /** * Return assignable bot logins from the repository assignee list. * @param {string} owner @@ -145,10 +176,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 +193,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 +564,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..a5322e14341 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -121,6 +121,36 @@ describe("assign_agent_helpers.cjs", () => { expect(mockCore.debug).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({}).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); + }); }); describe("getAssignableBots", () => { @@ -210,6 +240,29 @@ 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({}); + 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); + }); }); describe("getIssueDetails", () => { @@ -400,8 +453,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({}); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -427,6 +480,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 +492,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({}); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails - agent already assigned mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -460,7 +514,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({}); 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 +531,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..63461f9f889 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`); } 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; From 7c133ed5b23d4355d860ea6c076c017850f53714 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:03:00 +0000 Subject: [PATCH 02/12] Add debug logs for assignee alias validation path Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 3301749c4c0..f6652e06da7 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -72,7 +72,8 @@ 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] @@ -109,6 +110,7 @@ async function getAvailableAgentLogins(owner, repo, issueNumber = null, githubCl */ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubClient) { if (issueNumber) { + core.debug(`Checking assignee alias ${assignee} via issue-scoped endpoint for ${owner}/${repo}#${issueNumber}`); try { await githubClient.request("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { owner, @@ -116,19 +118,24 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC issue_number: Number(issueNumber), assignee, }); + core.debug(`Assignee alias ${assignee} is assignable via issue-scoped check`); return; } catch (e) { const status = e && typeof e === "object" && "status" in e ? e.status : undefined; if (status !== 404 && status !== 422) { + core.debug(`Issue-scoped assignee check failed for ${assignee} with status ${status ?? "unknown"}: ${getErrorMessage(e)}`); throw e; } + core.debug(`Issue-scoped assignee check returned ${status} for ${assignee}; falling back to repository-scoped check`); } } + core.debug(`Checking assignee alias ${assignee} via repository-scoped endpoint for ${owner}/${repo}`); await githubClient.rest.issues.checkUserCanBeAssigned({ owner, repo, assignee, }); + core.debug(`Assignee alias ${assignee} is assignable via repository-scoped check`); } /** From 4082923edea8488d09ef1ba3a9c8b809160ccfd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:34:53 +0000 Subject: [PATCH 03/12] Harden assignee alias validation fallback and fix agent assignment tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 26 +++++++--- .../setup/js/assign_agent_helpers.test.cjs | 43 ++++++++++++++-- actions/setup/js/assign_to_agent.cjs | 2 +- actions/setup/js/assign_to_agent.test.cjs | 49 +++++++++++++------ 4 files changed, 93 insertions(+), 27 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index f6652e06da7..c4ead32a3ce 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -109,25 +109,39 @@ async function getAvailableAgentLogins(owner, repo, issueNumber = null, githubCl * @param {Object} githubClient */ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubClient) { - if (issueNumber) { - core.debug(`Checking assignee alias ${assignee} via issue-scoped endpoint for ${owner}/${repo}#${issueNumber}`); + const parsedIssueNumber = Number(issueNumber); + const hasValidIssueNumber = Number.isInteger(parsedIssueNumber) && parsedIssueNumber > 0; + const hasIssueScopedRequest = typeof githubClient?.request === "function"; + + if (issueNumber && hasValidIssueNumber && hasIssueScopedRequest) { + core.debug(`Checking assignee alias ${assignee} via issue-scoped endpoint for ${owner}/${repo}#${parsedIssueNumber}`); try { - await githubClient.request("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { + const issueScopedResponse = await githubClient.request("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}", { owner, repo, - issue_number: Number(issueNumber), + issue_number: parsedIssueNumber, assignee, }); - core.debug(`Assignee alias ${assignee} is assignable via issue-scoped check`); - return; + const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; + if (Number.isInteger(issueScopedStatus) && issueScopedStatus >= 200 && issueScopedStatus < 300) { + core.debug(`Assignee alias ${assignee} is assignable via issue-scoped check`); + return; + } + core.debug(`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.debug(`Issue-scoped assignee check failed for ${assignee} with status ${status ?? "unknown"}: ${getErrorMessage(e)}`); throw e; } core.debug(`Issue-scoped assignee check returned ${status} for ${assignee}; falling back to repository-scoped check`); } + } else if (issueNumber && !hasValidIssueNumber) { + core.debug(`Skipping issue-scoped assignee check for ${assignee}: invalid issue number ${String(issueNumber)}`); + } else if (issueNumber && !hasIssueScopedRequest) { + core.debug(`Skipping issue-scoped assignee check for ${assignee}: github client does not support request()`); } core.debug(`Checking assignee alias ${assignee} via repository-scoped endpoint for ${owner}/${repo}`); await githubClient.rest.issues.checkUserCanBeAssigned({ diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index a5322e14341..69924161e67 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -124,7 +124,8 @@ describe("assign_agent_helpers.cjs", () => { 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({}).mockRejectedValue(err404); + mockGithub.request.mockRejectedValueOnce(err404).mockResolvedValueOnce({ status: 204 }).mockRejectedValue(err404); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(err404); const result = await getAvailableAgentLogins("owner", "repo", 123); @@ -243,7 +244,8 @@ describe("assign_agent_helpers.cjs", () => { 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({}); + 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); @@ -263,6 +265,37 @@ describe("assign_agent_helpers.cjs", () => { }); 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", () => { @@ -454,7 +487,7 @@ describe("assign_agent_helpers.cjs", () => { describe("assignAgentToIssueByName", () => { it("should successfully assign copilot agent", async () => { // findAgent: issue-scoped assignee check + getByUsername - mockGithub.request.mockResolvedValueOnce({}); + mockGithub.request.mockResolvedValueOnce({ status: 204 }); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -492,7 +525,7 @@ describe("assign_agent_helpers.cjs", () => { it("should report already assigned when agent is in assignees", async () => { // findAgent - mockGithub.request.mockResolvedValueOnce({}); + mockGithub.request.mockResolvedValueOnce({ status: 204 }); mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); // getIssueDetails - agent already assigned mockGithub.rest.issues.get.mockResolvedValueOnce({ @@ -514,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.request.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({ diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 63461f9f889..9263ec31db2 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -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} })()`); From 24798f8c2d54c1f41e1c897a8a903bbb83c872e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:48:52 +0000 Subject: [PATCH 04/12] Use core.info in assignee validation logs and fix typecheck guards Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 22 +++++++++---------- .../setup/js/assign_agent_helpers.test.cjs | 2 +- actions/setup/js/parse_codex_log.cjs | 6 ++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index c4ead32a3ce..c8f44386dbb 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -91,7 +91,7 @@ async function getAvailableAgentLogins(owner, repo, issueNumber = null, githubCl } 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)}`); } } } @@ -114,7 +114,7 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC const hasIssueScopedRequest = typeof githubClient?.request === "function"; if (issueNumber && hasValidIssueNumber && hasIssueScopedRequest) { - core.debug(`Checking assignee alias ${assignee} via issue-scoped endpoint for ${owner}/${repo}#${parsedIssueNumber}`); + 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, @@ -123,33 +123,33 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC assignee, }); const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; - if (Number.isInteger(issueScopedStatus) && issueScopedStatus >= 200 && issueScopedStatus < 300) { - core.debug(`Assignee alias ${assignee} is assignable via issue-scoped check`); + if (typeof issueScopedStatus === "number" && issueScopedStatus >= 200 && issueScopedStatus < 300) { + core.info(`Assignee alias ${assignee} is assignable via issue-scoped check`); return; } - core.debug(`Issue-scoped assignee check returned unexpected response for ${assignee} (status ${issueScopedStatus ?? "unknown"}); falling back to repository-scoped check`); + 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.debug(`Issue-scoped assignee check failed for ${assignee} with status ${status ?? "unknown"}: ${getErrorMessage(e)}`); + core.info(`Issue-scoped assignee check failed for ${assignee} with status ${status ?? "unknown"}: ${getErrorMessage(e)}`); throw e; } - core.debug(`Issue-scoped assignee check returned ${status} for ${assignee}; falling back to repository-scoped check`); + core.info(`Issue-scoped assignee check returned ${status} for ${assignee}; falling back to repository-scoped check`); } } else if (issueNumber && !hasValidIssueNumber) { - core.debug(`Skipping issue-scoped assignee check for ${assignee}: invalid issue number ${String(issueNumber)}`); + core.info(`Skipping issue-scoped assignee check for ${assignee}: invalid issue number ${String(issueNumber)}`); } else if (issueNumber && !hasIssueScopedRequest) { - core.debug(`Skipping issue-scoped assignee check for ${assignee}: github client does not support request()`); + core.info(`Skipping issue-scoped assignee check for ${assignee}: github client does not support request()`); } - core.debug(`Checking assignee alias ${assignee} via repository-scoped endpoint for ${owner}/${repo}`); + core.info(`Checking assignee alias ${assignee} via repository-scoped endpoint for ${owner}/${repo}`); await githubClient.rest.issues.checkUserCanBeAssigned({ owner, repo, assignee, }); - core.debug(`Assignee alias ${assignee} is assignable via repository-scoped check`); + core.info(`Assignee alias ${assignee} is assignable via repository-scoped check`); } /** diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index 69924161e67..ca0229ef42a 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -118,7 +118,7 @@ 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); }); diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index 341a9c4ae78..9c8d8f72075 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -423,10 +423,10 @@ 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 [server = "tool", toolName = "tool"] = (item.toolName || "tool__tool").split("__"); + markdown += formatCodexToolCall(server, toolName, item.params || "", item.response || "", item.statusIcon || "🔧"); } else if (item.type === "bash") { - markdown += formatCodexBashCall(item.content, item.response, item.statusIcon); + markdown += formatCodexBashCall(item.content || "", item.response || "", item.statusIcon || "🔧"); } } markdown += "\n## 📊 Information\n\n"; From eefaa94de56ba61d555df675cb3d9536d49109e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:50:47 +0000 Subject: [PATCH 05/12] Refine parse_codex fallback defaults for missing tool metadata Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/parse_codex_log.cjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index 9c8d8f72075..f2042be9dfd 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,10 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const [server = "tool", toolName = "tool"] = (item.toolName || "tool__tool").split("__"); - markdown += formatCodexToolCall(server, toolName, item.params || "", item.response || "", item.statusIcon || "🔧"); + const [server = "unknown", toolName = "unknown"] = (item.toolName || "unknown__unknown").split("__"); + 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"; From 54478b5638710813302543a2661efc9039346d7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:52:36 +0000 Subject: [PATCH 06/12] Keep issue-scoped status check integer-only Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index c8f44386dbb..a5deb56e732 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -123,11 +123,12 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC assignee, }); const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; - if (typeof issueScopedStatus === "number" && issueScopedStatus >= 200 && issueScopedStatus < 300) { + const validIssueScopedStatus = Number.isInteger(issueScopedStatus) ? issueScopedStatus : undefined; + if (validIssueScopedStatus !== undefined && validIssueScopedStatus >= 200 && validIssueScopedStatus < 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`); + core.info(`Issue-scoped assignee check returned unexpected response for ${assignee} (status ${validIssueScopedStatus ?? "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 From 13b68bfa0a40de8ac555b35e57faee29ff977d13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:54:36 +0000 Subject: [PATCH 07/12] Tighten parse codex fallbacks and simplify status checks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 5 ++--- actions/setup/js/parse_codex_log.cjs | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index a5deb56e732..20aa6278727 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -123,12 +123,11 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC assignee, }); const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; - const validIssueScopedStatus = Number.isInteger(issueScopedStatus) ? issueScopedStatus : undefined; - if (validIssueScopedStatus !== undefined && validIssueScopedStatus >= 200 && validIssueScopedStatus < 300) { + if (Number.isInteger(issueScopedStatus) && /** @type {number} */ issueScopedStatus >= 200 && /** @type {number} */ 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 ${validIssueScopedStatus ?? "unknown"}); falling back to repository-scoped check`); + 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 diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index f2042be9dfd..68a6c49bf75 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -425,7 +425,9 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const [server = "unknown", toolName = "unknown"] = (item.toolName || "unknown__unknown").split("__"); + const toolParts = (item.toolName || "unknown__unknown").split("__", 2); + const server = toolParts[0] || "unknown"; + const toolName = toolParts[1] || "unknown"; 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 || DEFAULT_STATUS_ICON); From 221b70f03bf188cf566ad5981c1afaa5560c3e5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:57:12 +0000 Subject: [PATCH 08/12] Adjust parse/tool status guards for strict typecheck Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 3 ++- actions/setup/js/parse_codex_log.cjs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 20aa6278727..cd1612315ad 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -123,7 +123,8 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC assignee, }); const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; - if (Number.isInteger(issueScopedStatus) && /** @type {number} */ issueScopedStatus >= 200 && /** @type {number} */ issueScopedStatus < 300) { + const validIssueScopedStatus = typeof issueScopedStatus === "number" && Number.isInteger(issueScopedStatus) ? issueScopedStatus : null; + if (validIssueScopedStatus !== null && validIssueScopedStatus >= 200 && validIssueScopedStatus < 300) { core.info(`Assignee alias ${assignee} is assignable via issue-scoped check`); return; } diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index 68a6c49bf75..cd2875afa5a 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -425,9 +425,7 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const toolParts = (item.toolName || "unknown__unknown").split("__", 2); - const server = toolParts[0] || "unknown"; - const toolName = toolParts[1] || "unknown"; + const [server, toolName = "unknown"] = (item.toolName || "unknown__unknown").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 || DEFAULT_STATUS_ICON); From 0882cec609458735f7d81f4ed789675cc08a560a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:59:11 +0000 Subject: [PATCH 09/12] Simplify fallback status and tool-name guards Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 5 ++--- actions/setup/js/parse_codex_log.cjs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index cd1612315ad..9272865af19 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -122,9 +122,8 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC issue_number: parsedIssueNumber, assignee, }); - const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : undefined; - const validIssueScopedStatus = typeof issueScopedStatus === "number" && Number.isInteger(issueScopedStatus) ? issueScopedStatus : null; - if (validIssueScopedStatus !== null && validIssueScopedStatus >= 200 && validIssueScopedStatus < 300) { + const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : Number.NaN; + if (Number.isInteger(issueScopedStatus) && issueScopedStatus >= 200 && issueScopedStatus < 300) { core.info(`Assignee alias ${assignee} is assignable via issue-scoped check`); return; } diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index cd2875afa5a..b2450a6ad3e 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -425,7 +425,8 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const [server, toolName = "unknown"] = (item.toolName || "unknown__unknown").split("__", 2); + const toolNameValue = item.toolName || "unknown__unknown"; + 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 || DEFAULT_STATUS_ICON); From b47638652fc4737c82edbfb7c3a7f4b7ee66ecbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:01:08 +0000 Subject: [PATCH 10/12] Use explicit undefined status and descriptive tool fallbacks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 4 ++-- actions/setup/js/parse_codex_log.cjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 9272865af19..a0756435e97 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -122,8 +122,8 @@ async function validateAssigneeAlias(owner, repo, assignee, issueNumber, githubC issue_number: parsedIssueNumber, assignee, }); - const issueScopedStatus = issueScopedResponse && typeof issueScopedResponse === "object" && "status" in issueScopedResponse ? Number(issueScopedResponse.status) : Number.NaN; - if (Number.isInteger(issueScopedStatus) && issueScopedStatus >= 200 && issueScopedStatus < 300) { + 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; } diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs index b2450a6ad3e..bf76aad5905 100644 --- a/actions/setup/js/parse_codex_log.cjs +++ b/actions/setup/js/parse_codex_log.cjs @@ -425,7 +425,7 @@ function parseCodexJsonl(logContent) { markdown += "## 🤖 Commands and Tools\n\n"; for (const item of parsedData) { if (item.type === "tool") { - const toolNameValue = item.toolName || "unknown__unknown"; + 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") { From e2619270dacf6f5ac9440627fa386028cc6f614d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:18:32 +0000 Subject: [PATCH 11/12] test: account for issue-scoped assignee check in PR fallback test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.test.cjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 13a774912b9..024ef89f63a 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); + expect(global.github.request.mock.calls[0][0]).toBe("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}"); + expect(global.github.request.mock.calls[1][0]).toBe("POST /agents/repos/{owner}/{repo}/tasks"); }); it("should use configured fallback_labels for fallback issues instead of PR labels", async () => { From 2e5986d60192ee4b0e10c861c450e8023e9556e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:20:24 +0000 Subject: [PATCH 12/12] test: make copilot fallback request assertion order-independent Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 024ef89f63a..653744838f4 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3298,8 +3298,8 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { // One request for issue-scoped alias validation and one for REST task creation expect(global.github.request).toHaveBeenCalledTimes(2); - expect(global.github.request.mock.calls[0][0]).toBe("GET /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee}"); - expect(global.github.request.mock.calls[1][0]).toBe("POST /agents/repos/{owner}/{repo}/tasks"); + 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 () => {