diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index ef4997239ed..8a308cea146 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -351,8 +351,12 @@ async function main(config = {}) { core.info(`${type} ID: ${assignableId}`); - // Skip if agent is already assigned - if (currentAssignees.some(a => a.id === agentId)) { + const hasPerItemPullRequestRepoOverride = !!message.pull_request_repo; + + // Skip if agent is already assigned and no explicit per-item pull_request_repo is specified. + // When a different pull_request_repo is provided on the message, allow re-assignment + // so Copilot can be triggered for a different target repository on the same issue. + if (currentAssignees.some(a => a.id === agentId) && !hasPerItemPullRequestRepoOverride) { core.info(`${agentName} is already assigned to ${type} #${number}`); _allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: true }); return { success: true }; diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index 33fc6f7c3d2..2492d7b5c94 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -388,6 +388,127 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); }); + it("should allow re-assignment when agent is already assigned but pull_request_repo differs", async () => { + process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/default-pr-repo"; + process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS = "test-owner/other-platform-repo"; + setAgentOutput({ + items: [ + { + type: "assign_to_agent", + issue_number: 42, + agent: "copilot", + pull_request_repo: "test-owner/other-platform-repo", + }, + ], + errors: [], + }); + + // Mock GraphQL responses + mockGithub.graphql + // Get global PR repository ID and default branch + .mockResolvedValueOnce({ + repository: { + id: "default-pr-repo-id", + defaultBranchRef: { name: "main" }, + }, + }) + // Get per-item PR repository ID + .mockResolvedValueOnce({ + repository: { + id: "other-platform-repo-id", + }, + }) + // Find agent + .mockResolvedValueOnce({ + repository: { + suggestedActors: { + nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], + }, + }, + }) + // Get issue details - agent is already assigned + .mockResolvedValueOnce({ + repository: { + issue: { + id: "issue-id", + assignees: { + nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], + }, + }, + }, + }) + // Assign agent (should proceed despite already being assigned) + .mockResolvedValueOnce({ + replaceActorsForAssignable: { + __typename: "ReplaceActorsForAssignablePayload", + }, + }); + + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); + + // Should NOT see "already assigned" skip message + expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("is already assigned to issue #42")); + // Should see successful assignment + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned copilot coding agent to issue #42")); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + // Verify the mutation was called with the per-item PR repo ID + const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; + expect(lastGraphQLCall[0]).toContain("agentAssignment"); + expect(lastGraphQLCall[1].targetRepoId).toBe("other-platform-repo-id"); + }); + + it("should still skip when agent is already assigned with global pull-request-repo but no per-item override", async () => { + process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/global-pr-repo"; + setAgentOutput({ + items: [ + { + type: "assign_to_agent", + issue_number: 42, + agent: "copilot", + }, + ], + errors: [], + }); + + // Mock GraphQL responses + mockGithub.graphql + // Get global PR repository ID and default branch + .mockResolvedValueOnce({ + repository: { + id: "global-pr-repo-id", + defaultBranchRef: { name: "main" }, + }, + }) + // Find agent + .mockResolvedValueOnce({ + repository: { + suggestedActors: { + nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], + }, + }, + }) + // Get issue details - agent is already assigned + .mockResolvedValueOnce({ + repository: { + issue: { + id: "issue-id", + assignees: { + nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], + }, + }, + }, + }); + + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); + + // Should see "already assigned" skip message + 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.graphql).toHaveBeenCalledTimes(3); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + it("should handle API errors gracefully", async () => { setAgentOutput({ items: [