From 2068d5be5778f5c8ac87b0f2878286f43db83274 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:02:25 +0000 Subject: [PATCH 1/7] Initial plan From b647651dc8247742af05e7a592234c5b8cdc19c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:08:49 +0000 Subject: [PATCH 2/7] Add pr-repo parameter to assign-to-agent safe output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 57 ++++++++--- actions/setup/js/assign_to_agent.cjs | 97 ++++++++++++++++++- .../content/docs/reference/safe-outputs.md | 16 ++- pkg/workflow/assign_to_agent.go | 4 +- .../compiler_safe_outputs_specialized.go | 17 ++++ pkg/workflow/js/safe_outputs_tools.json | 6 +- 6 files changed, 180 insertions(+), 17 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index d5bb1fa47ef..2e5c705e064 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -246,9 +246,10 @@ async function getPullRequestDetails(owner, repo, pullNumber) { * @param {Array<{id: string, login: string}>} currentAssignees - List of current assignees with id and login * @param {string} agentName - Agent name for error messages * @param {string[]|null} allowedAgents - Optional list of allowed agent names. If provided, filters out non-allowed agents from current assignees. + * @param {string|null} prRepoId - Optional PR repository ID for specifying where the PR should be created (GitHub agentAssignment.targetRepositoryId) * @returns {Promise} True if successful */ -async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null) { +async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null, prRepoId = null) { // Filter current assignees based on allowed list (if configured) let filteredAssignees = currentAssignees; if (allowedAgents && allowedAgents.length > 0) { @@ -271,24 +272,54 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent // Build actor IDs array - include new agent and preserve filtered assignees const actorIds = [agentId, ...filteredAssignees.map(a => a.id).filter(id => id !== agentId)]; - const mutation = ` - mutation($assignableId: ID!, $actorIds: [ID!]!) { - replaceActorsForAssignable(input: { - assignableId: $assignableId, - actorIds: $actorIds - }) { - __typename + // Build the mutation - conditionally include agentAssignment if prRepoId is provided + let mutation; + let variables; + + if (prRepoId) { + // Include agentAssignment with targetRepositoryId for cross-repo PR creation + mutation = ` + mutation($assignableId: ID!, $actorIds: [ID!]!, $targetRepoId: ID!) { + replaceActorsForAssignable(input: { + assignableId: $assignableId, + actorIds: $actorIds, + agentAssignment: { + targetRepositoryId: $targetRepoId + } + }) { + __typename + } } - } - `; + `; + variables = { + assignableId: assignableId, + actorIds, + targetRepoId: prRepoId, + }; + } else { + // Standard mutation without agentAssignment + mutation = ` + mutation($assignableId: ID!, $actorIds: [ID!]!) { + replaceActorsForAssignable(input: { + assignableId: $assignableId, + actorIds: $actorIds + }) { + __typename + } + } + `; + variables = { + assignableId: assignableId, + actorIds, + }; + } try { core.info("Using built-in github object for mutation"); - core.debug(`GraphQL mutation with variables: assignableId=${assignableId}, actorIds=${JSON.stringify(actorIds)}`); + core.debug(`GraphQL mutation with variables: assignableId=${assignableId}, actorIds=${JSON.stringify(actorIds)}${prRepoId ? `, targetRepoId=${prRepoId}` : ""}`); const response = await github.graphql(mutation, { - assignableId: assignableId, - actorIds, + ...variables, headers: { "GraphQL-Features": "issues_copilot_assignment_api_support", }, diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 0e58ab23b43..ff1679338d9 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -120,6 +120,51 @@ async function main() { // The github-token is set at the step level, so the built-in github object is authenticated // with the correct token (GH_AW_AGENT_TOKEN by default) + // Get PR repository configuration (where the PR should be created, may differ from issue repo) + const prRepoEnv = process.env.GH_AW_AGENT_PR_REPO?.trim(); + let prOwner = null; + let prRepo = null; + let prRepoId = null; + + // Get allowed PR repos configuration for cross-repo validation + const allowedPRReposEnv = process.env.GH_AW_AGENT_ALLOWED_PR_REPOS?.trim(); + const allowedPRRepos = parseAllowedRepos(allowedPRReposEnv); + + if (prRepoEnv) { + const parts = prRepoEnv.split("/"); + if (parts.length === 2) { + // Validate PR repository against allowlist + const repoValidation = validateRepo(prRepoEnv, defaultRepo, allowedPRRepos); + if (!repoValidation.valid) { + core.setFailed(`E004: ${repoValidation.error}`); + return; + } + + prOwner = parts[0]; + prRepo = parts[1]; + core.info(`Using PR repository: ${prOwner}/${prRepo}`); + + // Fetch the repository ID for the PR repo (needed for GraphQL agentAssignment) + try { + const prRepoQuery = ` + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + id + } + } + `; + const prRepoResponse = await github.graphql(prRepoQuery, { owner: prOwner, name: prRepo }); + prRepoId = prRepoResponse.repository.id; + core.info(`PR repository ID: ${prRepoId}`); + } catch (error) { + core.setFailed(`Failed to fetch PR repository ID for ${prOwner}/${prRepo}: ${error.message}`); + return; + } + } else { + core.warning(`Invalid pr-repo format: ${prRepoEnv}. Expected owner/repo. PRs will be created in issue repository.`); + } + } + // Cache agent IDs to avoid repeated lookups const agentCache = {}; @@ -181,6 +226,55 @@ async function main() { const hasExplicitTarget = itemForTarget.issue_number != null || itemForTarget.pull_number != null; const effectiveTarget = hasExplicitTarget ? "*" : targetConfig; + // Handle per-item pr_repo parameter (where the PR should be created) + // This overrides the global pr-repo configuration if specified + let effectivePRRepoId = prRepoId; + if (item.pr_repo) { + const itemPRRepo = item.pr_repo.trim(); + const prRepoParts = itemPRRepo.split("/"); + if (prRepoParts.length === 2) { + // Validate PR repository against allowlist + const prRepoValidation = validateRepo(itemPRRepo, defaultRepo, allowedPRRepos); + if (!prRepoValidation.valid) { + core.error(`E004: ${prRepoValidation.error}`); + results.push({ + issue_number: item.issue_number || null, + pull_number: item.pull_number || null, + agent: agentName, + success: false, + error: prRepoValidation.error, + }); + continue; + } + + // Fetch the repository ID for the item's PR repo + try { + const itemPRRepoQuery = ` + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + id + } + } + `; + const itemPRRepoResponse = await github.graphql(itemPRRepoQuery, { owner: prRepoParts[0], name: prRepoParts[1] }); + effectivePRRepoId = itemPRRepoResponse.repository.id; + core.info(`Using per-item PR repository: ${itemPRRepo} (ID: ${effectivePRRepoId})`); + } catch (error) { + core.error(`Failed to fetch PR repository ID for ${itemPRRepo}: ${error.message}`); + results.push({ + issue_number: item.issue_number || null, + pull_number: item.pull_number || null, + agent: agentName, + success: false, + error: `Failed to fetch PR repository ID for ${itemPRRepo}`, + }); + continue; + } + } else { + core.warning(`Invalid pr_repo format: ${itemPRRepo}. Expected owner/repo. Using global pr-repo if configured.`); + } + } + // Resolve target number using the same logic as other safe outputs // This allows automatic resolution from workflow context when issue_number/pull_number is not explicitly provided const targetResult = resolveTarget({ @@ -306,8 +400,9 @@ async function main() { // Assign agent using GraphQL mutation - uses built-in github object authenticated via github-token // Pass the allowed list so existing assignees are filtered before calling replaceActorsForAssignable + // Pass the PR repo ID if configured (to specify where the PR should be created) core.info(`Assigning ${agentName} coding agent to ${type} #${number}...`); - const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents); + const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, effectivePRRepoId); if (!success) { throw new Error(`Failed to assign ${agentName} via GraphQL`); diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 053167be84b..db1ffb61421 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -1304,7 +1304,9 @@ safe-outputs: allowed: [copilot] # restrict to specific agents (optional) max: 1 # max assignments (default: 1) target: "triggering" # "triggering" (default), "*", or number - target-repo: "owner/repo" # cross-repository + target-repo: "owner/repo" # where the issue lives (cross-repository) + pr-repo: "owner/repo" # where the PR should be created (may differ from issue repo) + allowed-pr-repos: [owner/repo1, owner/repo2] # additional allowed PR repositories ``` **Behavior:** @@ -1312,6 +1314,18 @@ safe-outputs: - `target: "*"` - Requires explicit `issue_number` or `pull_number` in agent output - `target: "123"` - Always uses issue/PR #123 +**Cross-Repository PR Creation:** +The `pr-repo` parameter allows you to create pull requests in a different repository than where the issue lives. This is useful when: +- Issues are tracked in a central repository but code lives in separate repositories +- You want to separate issue tracking from code repositories + +When `pr-repo` is configured, Copilot will create the pull request in the specified repository instead of the issue's repository. The issue repository is determined by `target-repo` or defaults to the workflow's repository. + +You can also specify `pr_repo` on a per-assignment basis in the agent output using the `assign_to_agent` tool: +```python +assign_to_agent(issue_number=123, agent="copilot", pr_repo="owner/codebase-repo") +``` + **Assignee Filtering:** When `allowed` list is configured, existing agent assignees not in the list are removed while regular user assignees are preserved. diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index f23e682ab85..ddccae12978 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -13,6 +13,8 @@ type AssignToAgentConfig struct { DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails + PRRepoSlug string `yaml:"pr-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) + AllowedPRRepos []string `yaml:"allowed-pr-repos,omitempty"` // List of additional repositories that PRs can be created in (in addition to pr-repo) } // parseAssignToAgentConfig handles assign-to-agent configuration @@ -37,7 +39,7 @@ func (c *Compiler) parseAssignToAgentConfig(outputMap map[string]any) *AssignToA config.Max = 1 } - assignToAgentLog.Printf("Parsed assign-to-agent config: default_agent=%s, allowed_count=%d, target=%s, max=%d", config.DefaultAgent, len(config.Allowed), config.Target, config.Max) + assignToAgentLog.Printf("Parsed assign-to-agent config: default_agent=%s, allowed_count=%d, target=%s, max=%d, pr_repo=%s", config.DefaultAgent, len(config.Allowed), config.Target, config.Max, config.PRRepoSlug) return &config } diff --git a/pkg/workflow/compiler_safe_outputs_specialized.go b/pkg/workflow/compiler_safe_outputs_specialized.go index 0c51e63cd86..c4237613188 100644 --- a/pkg/workflow/compiler_safe_outputs_specialized.go +++ b/pkg/workflow/compiler_safe_outputs_specialized.go @@ -48,6 +48,23 @@ func (c *Compiler) buildAssignToAgentStepConfig(data *WorkflowData, mainJobName customEnvVars = append(customEnvVars, " GH_AW_AGENT_IGNORE_IF_ERROR: \"true\"\n") } + // Add PR repository configuration environment variable (where the PR should be created) + if cfg.PRRepoSlug != "" { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_PR_REPO: %q\n", cfg.PRRepoSlug)) + } + + // Add allowed PR repos list environment variable (comma-separated) + if len(cfg.AllowedPRRepos) > 0 { + allowedPRReposStr := "" + for i, repo := range cfg.AllowedPRRepos { + if i > 0 { + allowedPRReposStr += "," + } + allowedPRReposStr += repo + } + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_ALLOWED_PR_REPOS: %q\n", allowedPRReposStr)) + } + // Allow assign_to_agent to reference issues created earlier in the same run via temporary IDs (aw_...) // The handler manager (process_safe_outputs) produces a temporary_id_map output when create_issue is enabled. if data.SafeOutputs != nil && data.SafeOutputs.CreateIssues != nil { diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index b35bb9867cd..85c55a61dde 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -504,7 +504,7 @@ }, { "name": "assign_to_agent", - "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\")", + "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\", pr_repo=\"owner/repo\")", "inputSchema": { "type": "object", "properties": { @@ -525,6 +525,10 @@ "agent": { "type": "string", "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified." + }, + "pr_repo": { + "type": "string", + "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. Must be in allowed-pr-repos list if specified." } }, "additionalProperties": false From e0f30d242c548d9df2423cd5be1c4c22c068f2a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:11:00 +0000 Subject: [PATCH 3/7] Add pr-repo and allowed-pr-repos to JSON schema Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 11 +++++++++++ pkg/workflow/assign_to_agent.go | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 455d13da753..f89da3b11d9 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5380,6 +5380,17 @@ "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository agent assignment. Takes precedence over trial target repo settings." }, + "pr-repo": { + "type": "string", + "description": "Target repository where the pull request should be created, in format 'owner/repo'. If omitted, the PR will be created in the same repository as the issue (specified by target-repo or the workflow's repository). This allows issues and code to live in different repositories." + }, + "allowed-pr-repos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of additional repositories that pull requests can be created in (in addition to pr-repo). Each entry should be in 'owner/repo' format." + }, "ignore-if-error": { "type": "boolean", "description": "If true, the workflow continues gracefully when agent assignment fails (e.g., due to missing token or insufficient permissions), logging a warning instead of failing. Default is false. Useful for workflows that should not fail when agent assignment is optional.", diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index ddccae12978..996da42214c 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -10,10 +10,10 @@ var assignToAgentLog = logger.New("workflow:assign_to_agent") type AssignToAgentConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` - DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") - Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. - IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails - PRRepoSlug string `yaml:"pr-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) + DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") + Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. + IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails + PRRepoSlug string `yaml:"pr-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) AllowedPRRepos []string `yaml:"allowed-pr-repos,omitempty"` // List of additional repositories that PRs can be created in (in addition to pr-repo) } From 2e7b944e4f98d7777a7f910337435a1b2fe25d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:13:09 +0000 Subject: [PATCH 4/7] Add tests for pr-repo functionality and fix TypeScript errors Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 4 +- actions/setup/js/assign_to_agent.test.cjs | 115 ++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index ff1679338d9..dc8100dfb37 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -157,7 +157,7 @@ async function main() { prRepoId = prRepoResponse.repository.id; core.info(`PR repository ID: ${prRepoId}`); } catch (error) { - core.setFailed(`Failed to fetch PR repository ID for ${prOwner}/${prRepo}: ${error.message}`); + core.setFailed(`Failed to fetch PR repository ID for ${prOwner}/${prRepo}: ${getErrorMessage(error)}`); return; } } else { @@ -260,7 +260,7 @@ async function main() { effectivePRRepoId = itemPRRepoResponse.repository.id; core.info(`Using per-item PR repository: ${itemPRRepo} (ID: ${effectivePRRepoId})`); } catch (error) { - core.error(`Failed to fetch PR repository ID for ${itemPRRepo}: ${error.message}`); + core.error(`Failed to fetch PR repository ID for ${itemPRRepo}: ${getErrorMessage(error)}`); results.push({ issue_number: item.issue_number || null, pull_number: item.pull_number || null, diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index ec2e7fdd31e..5d48c8f63d7 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -56,6 +56,8 @@ describe("assign_to_agent", () => { delete process.env.GH_AW_TARGET_REPO; delete process.env.GH_AW_AGENT_IGNORE_IF_ERROR; delete process.env.GH_AW_TEMPORARY_ID_MAP; + delete process.env.GH_AW_AGENT_PR_REPO; + delete process.env.GH_AW_AGENT_ALLOWED_PR_REPOS; // Reset context to default mockContext.eventName = "issues"; @@ -1144,4 +1146,117 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Looking for copilot coding agent")); }, 20000); }); + + it("should handle pr-repo configuration correctly", async () => { + process.env.GH_AW_AGENT_PR_REPO = "test-owner/pr-repo"; + process.env.GH_AW_AGENT_ALLOWED_PR_REPOS = "test-owner/pr-repo"; + setAgentOutput({ + items: [ + { + type: "assign_to_agent", + issue_number: 42, + agent: "copilot", + }, + ], + errors: [], + }); + + // Mock GraphQL responses + mockGithub.graphql + // Get PR repository ID + .mockResolvedValueOnce({ + repository: { + id: "pr-repo-id", + }, + }) + // Find agent + .mockResolvedValueOnce({ + repository: { + suggestedActors: { + nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], + }, + }, + }) + // Get issue details + .mockResolvedValueOnce({ + repository: { + issue: { + id: "issue-id", + assignees: { nodes: [] }, + }, + }, + }) + // Assign agent with agentAssignment + .mockResolvedValueOnce({ + replaceActorsForAssignable: { + __typename: "ReplaceActorsForAssignablePayload", + }, + }); + + await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using PR repository: test-owner/pr-repo")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("PR repository ID: pr-repo-id")); + + // Verify the mutation was called with agentAssignment + const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; + expect(lastGraphQLCall[0]).toContain("agentAssignment"); + expect(lastGraphQLCall[0]).toContain("targetRepositoryId"); + expect(lastGraphQLCall[1].targetRepoId).toBe("pr-repo-id"); + }); + + it("should handle per-item pr_repo parameter", async () => { + process.env.GH_AW_AGENT_ALLOWED_PR_REPOS = "test-owner/item-pr-repo"; + setAgentOutput({ + items: [ + { + type: "assign_to_agent", + issue_number: 42, + agent: "copilot", + pr_repo: "test-owner/item-pr-repo", + }, + ], + errors: [], + }); + + // Mock GraphQL responses + mockGithub.graphql + // Get item PR repository ID + .mockResolvedValueOnce({ + repository: { + id: "item-pr-repo-id", + }, + }) + // Find agent + .mockResolvedValueOnce({ + repository: { + suggestedActors: { + nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], + }, + }, + }) + // Get issue details + .mockResolvedValueOnce({ + repository: { + issue: { + id: "issue-id", + assignees: { nodes: [] }, + }, + }, + }) + // Assign agent with agentAssignment + .mockResolvedValueOnce({ + replaceActorsForAssignable: { + __typename: "ReplaceActorsForAssignablePayload", + }, + }); + + await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using per-item PR repository: test-owner/item-pr-repo")); + + // Verify the mutation was called with per-item PR repo ID + const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; + expect(lastGraphQLCall[1].targetRepoId).toBe("item-pr-repo-id"); + }); }); From f4ce8eb81a4996b7c6813554e3da3570f1ee24eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:24:15 +0000 Subject: [PATCH 5/7] Rename pr-repo to pull-request-repo in field names Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 12 +-- actions/setup/js/assign_to_agent.cjs | 74 +++++++++---------- actions/setup/js/assign_to_agent.test.cjs | 30 ++++---- .../content/docs/reference/safe-outputs.md | 12 +-- pkg/parser/schemas/main_workflow_schema.json | 6 +- pkg/workflow/assign_to_agent.go | 16 ++-- .../compiler_safe_outputs_specialized.go | 16 ++-- pkg/workflow/js/safe_outputs_tools.json | 6 +- 8 files changed, 86 insertions(+), 86 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 2e5c705e064..22bdf549955 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -246,10 +246,10 @@ async function getPullRequestDetails(owner, repo, pullNumber) { * @param {Array<{id: string, login: string}>} currentAssignees - List of current assignees with id and login * @param {string} agentName - Agent name for error messages * @param {string[]|null} allowedAgents - Optional list of allowed agent names. If provided, filters out non-allowed agents from current assignees. - * @param {string|null} prRepoId - Optional PR repository ID for specifying where the PR should be created (GitHub agentAssignment.targetRepositoryId) + * @param {string|null} pullRequestRepoId - Optional pull request repository ID for specifying where the PR should be created (GitHub agentAssignment.targetRepositoryId) * @returns {Promise} True if successful */ -async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null, prRepoId = null) { +async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null, pullRequestRepoId = null) { // Filter current assignees based on allowed list (if configured) let filteredAssignees = currentAssignees; if (allowedAgents && allowedAgents.length > 0) { @@ -272,11 +272,11 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent // Build actor IDs array - include new agent and preserve filtered assignees const actorIds = [agentId, ...filteredAssignees.map(a => a.id).filter(id => id !== agentId)]; - // Build the mutation - conditionally include agentAssignment if prRepoId is provided + // Build the mutation - conditionally include agentAssignment if pullRequestRepoId is provided let mutation; let variables; - if (prRepoId) { + if (pullRequestRepoId) { // Include agentAssignment with targetRepositoryId for cross-repo PR creation mutation = ` mutation($assignableId: ID!, $actorIds: [ID!]!, $targetRepoId: ID!) { @@ -294,7 +294,7 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent variables = { assignableId: assignableId, actorIds, - targetRepoId: prRepoId, + targetRepoId: pullRequestRepoId, }; } else { // Standard mutation without agentAssignment @@ -317,7 +317,7 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent try { core.info("Using built-in github object for mutation"); - core.debug(`GraphQL mutation with variables: assignableId=${assignableId}, actorIds=${JSON.stringify(actorIds)}${prRepoId ? `, targetRepoId=${prRepoId}` : ""}`); + core.debug(`GraphQL mutation with variables: assignableId=${assignableId}, actorIds=${JSON.stringify(actorIds)}${pullRequestRepoId ? `, targetRepoId=${pullRequestRepoId}` : ""}`); const response = await github.graphql(mutation, { ...variables, headers: { diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index dc8100dfb37..75329e9d624 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -121,47 +121,47 @@ async function main() { // with the correct token (GH_AW_AGENT_TOKEN by default) // Get PR repository configuration (where the PR should be created, may differ from issue repo) - const prRepoEnv = process.env.GH_AW_AGENT_PR_REPO?.trim(); - let prOwner = null; - let prRepo = null; - let prRepoId = null; + const pullRequestRepoEnv = process.env.GH_AW_AGENT_PULL_REQUEST_REPO?.trim(); + let pullRequestOwner = null; + let pullRequestRepo = null; + let pullRequestRepoId = null; // Get allowed PR repos configuration for cross-repo validation - const allowedPRReposEnv = process.env.GH_AW_AGENT_ALLOWED_PR_REPOS?.trim(); - const allowedPRRepos = parseAllowedRepos(allowedPRReposEnv); + const allowedPullRequestReposEnv = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS?.trim(); + const allowedPullRequestRepos = parseAllowedRepos(allowedPullRequestReposEnv); - if (prRepoEnv) { - const parts = prRepoEnv.split("/"); + if (pullRequestRepoEnv) { + const parts = pullRequestRepoEnv.split("/"); if (parts.length === 2) { // Validate PR repository against allowlist - const repoValidation = validateRepo(prRepoEnv, defaultRepo, allowedPRRepos); + const repoValidation = validateRepo(pullRequestRepoEnv, defaultRepo, allowedPullRequestRepos); if (!repoValidation.valid) { core.setFailed(`E004: ${repoValidation.error}`); return; } - prOwner = parts[0]; - prRepo = parts[1]; - core.info(`Using PR repository: ${prOwner}/${prRepo}`); + pullRequestOwner = parts[0]; + pullRequestRepo = parts[1]; + core.info(`Using pull request repository: ${pullRequestOwner}/${pullRequestRepo}`); // Fetch the repository ID for the PR repo (needed for GraphQL agentAssignment) try { - const prRepoQuery = ` + const pullRequestRepoQuery = ` query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { id } } `; - const prRepoResponse = await github.graphql(prRepoQuery, { owner: prOwner, name: prRepo }); - prRepoId = prRepoResponse.repository.id; - core.info(`PR repository ID: ${prRepoId}`); + const pullRequestRepoResponse = await github.graphql(pullRequestRepoQuery, { owner: pullRequestOwner, name: pullRequestRepo }); + pullRequestRepoId = pullRequestRepoResponse.repository.id; + core.info(`Pull request repository ID: ${pullRequestRepoId}`); } catch (error) { - core.setFailed(`Failed to fetch PR repository ID for ${prOwner}/${prRepo}: ${getErrorMessage(error)}`); + core.setFailed(`Failed to fetch pull request repository ID for ${pullRequestOwner}/${pullRequestRepo}: ${getErrorMessage(error)}`); return; } } else { - core.warning(`Invalid pr-repo format: ${prRepoEnv}. Expected owner/repo. PRs will be created in issue repository.`); + core.warning(`Invalid pull-request-repo format: ${pullRequestRepoEnv}. Expected owner/repo. PRs will be created in issue repository.`); } } @@ -226,52 +226,52 @@ async function main() { const hasExplicitTarget = itemForTarget.issue_number != null || itemForTarget.pull_number != null; const effectiveTarget = hasExplicitTarget ? "*" : targetConfig; - // Handle per-item pr_repo parameter (where the PR should be created) - // This overrides the global pr-repo configuration if specified - let effectivePRRepoId = prRepoId; - if (item.pr_repo) { - const itemPRRepo = item.pr_repo.trim(); - const prRepoParts = itemPRRepo.split("/"); - if (prRepoParts.length === 2) { + // Handle per-item pull_request_repo parameter (where the PR should be created) + // This overrides the global pull-request-repo configuration if specified + let effectivePullRequestRepoId = pullRequestRepoId; + if (item.pull_request_repo) { + const itemPullRequestRepo = item.pull_request_repo.trim(); + const pullRequestRepoParts = itemPullRequestRepo.split("/"); + if (pullRequestRepoParts.length === 2) { // Validate PR repository against allowlist - const prRepoValidation = validateRepo(itemPRRepo, defaultRepo, allowedPRRepos); - if (!prRepoValidation.valid) { - core.error(`E004: ${prRepoValidation.error}`); + const pullRequestRepoValidation = validateRepo(itemPullRequestRepo, defaultRepo, allowedPullRequestRepos); + if (!pullRequestRepoValidation.valid) { + core.error(`E004: ${pullRequestRepoValidation.error}`); results.push({ issue_number: item.issue_number || null, pull_number: item.pull_number || null, agent: agentName, success: false, - error: prRepoValidation.error, + error: pullRequestRepoValidation.error, }); continue; } // Fetch the repository ID for the item's PR repo try { - const itemPRRepoQuery = ` + const itemPullRequestRepoQuery = ` query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { id } } `; - const itemPRRepoResponse = await github.graphql(itemPRRepoQuery, { owner: prRepoParts[0], name: prRepoParts[1] }); - effectivePRRepoId = itemPRRepoResponse.repository.id; - core.info(`Using per-item PR repository: ${itemPRRepo} (ID: ${effectivePRRepoId})`); + const itemPullRequestRepoResponse = await github.graphql(itemPullRequestRepoQuery, { owner: pullRequestRepoParts[0], name: pullRequestRepoParts[1] }); + effectivePullRequestRepoId = itemPullRequestRepoResponse.repository.id; + core.info(`Using per-item pull request repository: ${itemPullRequestRepo} (ID: ${effectivePullRequestRepoId})`); } catch (error) { - core.error(`Failed to fetch PR repository ID for ${itemPRRepo}: ${getErrorMessage(error)}`); + core.error(`Failed to fetch pull request repository ID for ${itemPullRequestRepo}: ${getErrorMessage(error)}`); results.push({ issue_number: item.issue_number || null, pull_number: item.pull_number || null, agent: agentName, success: false, - error: `Failed to fetch PR repository ID for ${itemPRRepo}`, + error: `Failed to fetch pull request repository ID for ${itemPullRequestRepo}`, }); continue; } } else { - core.warning(`Invalid pr_repo format: ${itemPRRepo}. Expected owner/repo. Using global pr-repo if configured.`); + core.warning(`Invalid pull_request_repo format: ${itemPullRequestRepo}. Expected owner/repo. Using global pull-request-repo if configured.`); } } @@ -402,7 +402,7 @@ async function main() { // Pass the allowed list so existing assignees are filtered before calling replaceActorsForAssignable // Pass the PR repo ID if configured (to specify where the PR should be created) core.info(`Assigning ${agentName} coding agent to ${type} #${number}...`); - const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, effectivePRRepoId); + const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, effectivePullRequestRepoId); if (!success) { throw new Error(`Failed to assign ${agentName} via GraphQL`); diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index 5d48c8f63d7..5df426f9399 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -56,8 +56,8 @@ describe("assign_to_agent", () => { delete process.env.GH_AW_TARGET_REPO; delete process.env.GH_AW_AGENT_IGNORE_IF_ERROR; delete process.env.GH_AW_TEMPORARY_ID_MAP; - delete process.env.GH_AW_AGENT_PR_REPO; - delete process.env.GH_AW_AGENT_ALLOWED_PR_REPOS; + delete process.env.GH_AW_AGENT_PULL_REQUEST_REPO; + delete process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS; // Reset context to default mockContext.eventName = "issues"; @@ -1147,9 +1147,9 @@ describe("assign_to_agent", () => { }, 20000); }); - it("should handle pr-repo configuration correctly", async () => { - process.env.GH_AW_AGENT_PR_REPO = "test-owner/pr-repo"; - process.env.GH_AW_AGENT_ALLOWED_PR_REPOS = "test-owner/pr-repo"; + it("should handle pull-request-repo configuration correctly", async () => { + process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/pull-request-repo"; + process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS = "test-owner/pull-request-repo"; setAgentOutput({ items: [ { @@ -1166,7 +1166,7 @@ describe("assign_to_agent", () => { // Get PR repository ID .mockResolvedValueOnce({ repository: { - id: "pr-repo-id", + id: "pull-request-repo-id", }, }) // Find agent @@ -1195,25 +1195,25 @@ describe("assign_to_agent", () => { await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using PR repository: test-owner/pr-repo")); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("PR repository ID: pr-repo-id")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using pull request repository: test-owner/pull-request-repo")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Pull request repository ID: pull-request-repo-id")); // Verify the mutation was called with agentAssignment const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; expect(lastGraphQLCall[0]).toContain("agentAssignment"); expect(lastGraphQLCall[0]).toContain("targetRepositoryId"); - expect(lastGraphQLCall[1].targetRepoId).toBe("pr-repo-id"); + expect(lastGraphQLCall[1].targetRepoId).toBe("pull-request-repo-id"); }); - it("should handle per-item pr_repo parameter", async () => { - process.env.GH_AW_AGENT_ALLOWED_PR_REPOS = "test-owner/item-pr-repo"; + it("should handle per-item pull_request_repo parameter", async () => { + process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS = "test-owner/item-pull-request-repo"; setAgentOutput({ items: [ { type: "assign_to_agent", issue_number: 42, agent: "copilot", - pr_repo: "test-owner/item-pr-repo", + pull_request_repo: "test-owner/item-pull-request-repo", }, ], errors: [], @@ -1224,7 +1224,7 @@ describe("assign_to_agent", () => { // Get item PR repository ID .mockResolvedValueOnce({ repository: { - id: "item-pr-repo-id", + id: "item-pull-request-repo-id", }, }) // Find agent @@ -1253,10 +1253,10 @@ describe("assign_to_agent", () => { await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using per-item PR repository: test-owner/item-pr-repo")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using per-item pull request repository: test-owner/item-pull-request-repo")); // Verify the mutation was called with per-item PR repo ID const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; - expect(lastGraphQLCall[1].targetRepoId).toBe("item-pr-repo-id"); + expect(lastGraphQLCall[1].targetRepoId).toBe("item-pull-request-repo-id"); }); }); diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index db1ffb61421..d1398f5a9a9 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -1305,8 +1305,8 @@ safe-outputs: max: 1 # max assignments (default: 1) target: "triggering" # "triggering" (default), "*", or number target-repo: "owner/repo" # where the issue lives (cross-repository) - pr-repo: "owner/repo" # where the PR should be created (may differ from issue repo) - allowed-pr-repos: [owner/repo1, owner/repo2] # additional allowed PR repositories + pull-request-repo: "owner/repo" # where the PR should be created (may differ from issue repo) + allowed-pull-request-repos: [owner/repo1, owner/repo2] # additional allowed PR repositories ``` **Behavior:** @@ -1315,15 +1315,15 @@ safe-outputs: - `target: "123"` - Always uses issue/PR #123 **Cross-Repository PR Creation:** -The `pr-repo` parameter allows you to create pull requests in a different repository than where the issue lives. This is useful when: +The `pull-request-repo` parameter allows you to create pull requests in a different repository than where the issue lives. This is useful when: - Issues are tracked in a central repository but code lives in separate repositories - You want to separate issue tracking from code repositories -When `pr-repo` is configured, Copilot will create the pull request in the specified repository instead of the issue's repository. The issue repository is determined by `target-repo` or defaults to the workflow's repository. +When `pull-request-repo` is configured, Copilot will create the pull request in the specified repository instead of the issue's repository. The issue repository is determined by `target-repo` or defaults to the workflow's repository. -You can also specify `pr_repo` on a per-assignment basis in the agent output using the `assign_to_agent` tool: +You can also specify `pull_request_repo` on a per-assignment basis in the agent output using the `assign_to_agent` tool: ```python -assign_to_agent(issue_number=123, agent="copilot", pr_repo="owner/codebase-repo") +assign_to_agent(issue_number=123, agent="copilot", pull_request_repo="owner/codebase-repo") ``` **Assignee Filtering:** diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index f89da3b11d9..5616a5cb182 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5380,16 +5380,16 @@ "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository agent assignment. Takes precedence over trial target repo settings." }, - "pr-repo": { + "pull-request-repo": { "type": "string", "description": "Target repository where the pull request should be created, in format 'owner/repo'. If omitted, the PR will be created in the same repository as the issue (specified by target-repo or the workflow's repository). This allows issues and code to live in different repositories." }, - "allowed-pr-repos": { + "allowed-pull-request-repos": { "type": "array", "items": { "type": "string" }, - "description": "List of additional repositories that pull requests can be created in (in addition to pr-repo). Each entry should be in 'owner/repo' format." + "description": "List of additional repositories that pull requests can be created in (in addition to pull-request-repo). Each entry should be in 'owner/repo' format." }, "ignore-if-error": { "type": "boolean", diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index 996da42214c..84e1c2eed93 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -8,13 +8,13 @@ var assignToAgentLog = logger.New("workflow:assign_to_agent") // AssignToAgentConfig holds configuration for assigning agents to issues from agent output type AssignToAgentConfig struct { - BaseSafeOutputConfig `yaml:",inline"` - SafeOutputTargetConfig `yaml:",inline"` - DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") - Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. - IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails - PRRepoSlug string `yaml:"pr-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) - AllowedPRRepos []string `yaml:"allowed-pr-repos,omitempty"` // List of additional repositories that PRs can be created in (in addition to pr-repo) + BaseSafeOutputConfig `yaml:",inline"` + SafeOutputTargetConfig `yaml:",inline"` + DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") + Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. + IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails + PullRequestRepoSlug string `yaml:"pull-request-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) + AllowedPullRequestRepos []string `yaml:"allowed-pull-request-repos,omitempty"` // List of additional repositories that PRs can be created in (in addition to pull-request-repo) } // parseAssignToAgentConfig handles assign-to-agent configuration @@ -39,7 +39,7 @@ func (c *Compiler) parseAssignToAgentConfig(outputMap map[string]any) *AssignToA config.Max = 1 } - assignToAgentLog.Printf("Parsed assign-to-agent config: default_agent=%s, allowed_count=%d, target=%s, max=%d, pr_repo=%s", config.DefaultAgent, len(config.Allowed), config.Target, config.Max, config.PRRepoSlug) + assignToAgentLog.Printf("Parsed assign-to-agent config: default_agent=%s, allowed_count=%d, target=%s, max=%d, pull_request_repo=%s", config.DefaultAgent, len(config.Allowed), config.Target, config.Max, config.PullRequestRepoSlug) return &config } diff --git a/pkg/workflow/compiler_safe_outputs_specialized.go b/pkg/workflow/compiler_safe_outputs_specialized.go index c4237613188..b78454a9ec1 100644 --- a/pkg/workflow/compiler_safe_outputs_specialized.go +++ b/pkg/workflow/compiler_safe_outputs_specialized.go @@ -49,20 +49,20 @@ func (c *Compiler) buildAssignToAgentStepConfig(data *WorkflowData, mainJobName } // Add PR repository configuration environment variable (where the PR should be created) - if cfg.PRRepoSlug != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_PR_REPO: %q\n", cfg.PRRepoSlug)) + if cfg.PullRequestRepoSlug != "" { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_PULL_REQUEST_REPO: %q\n", cfg.PullRequestRepoSlug)) } // Add allowed PR repos list environment variable (comma-separated) - if len(cfg.AllowedPRRepos) > 0 { - allowedPRReposStr := "" - for i, repo := range cfg.AllowedPRRepos { + if len(cfg.AllowedPullRequestRepos) > 0 { + allowedPullRequestReposStr := "" + for i, repo := range cfg.AllowedPullRequestRepos { if i > 0 { - allowedPRReposStr += "," + allowedPullRequestReposStr += "," } - allowedPRReposStr += repo + allowedPullRequestReposStr += repo } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_ALLOWED_PR_REPOS: %q\n", allowedPRReposStr)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS: %q\n", allowedPullRequestReposStr)) } // Allow assign_to_agent to reference issues created earlier in the same run via temporary IDs (aw_...) diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 85c55a61dde..98918b0c565 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -504,7 +504,7 @@ }, { "name": "assign_to_agent", - "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\", pr_repo=\"owner/repo\")", + "description": "Assign the GitHub Copilot coding agent to work on an issue or pull request. The agent will analyze the issue/PR and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot. Example usage: assign_to_agent(issue_number=123, agent=\"copilot\") or assign_to_agent(pull_number=456, agent=\"copilot\", pull_request_repo=\"owner/repo\")", "inputSchema": { "type": "object", "properties": { @@ -526,9 +526,9 @@ "type": "string", "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified." }, - "pr_repo": { + "pull_request_repo": { "type": "string", - "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. Must be in allowed-pr-repos list if specified." + "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. Must be in allowed-pull-request-repos list if specified." } }, "additionalProperties": false From 9d6f6831daaadc2b68ef16e9649016dffa1319fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:50:43 +0000 Subject: [PATCH 6/7] Update safe outputs specification with pull-request-repo pattern Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- scratchpad/safe-outputs-specification.md | 45 +++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/scratchpad/safe-outputs-specification.md b/scratchpad/safe-outputs-specification.md index f2500ba6ad6..51ac3cd2f3c 100644 --- a/scratchpad/safe-outputs-specification.md +++ b/scratchpad/safe-outputs-specification.md @@ -309,6 +309,13 @@ For cross-repository operations, implementations MUST: - Reject operations when token lacks required permissions - Apply same validation rules as same-repository operations +For operations that support separate PR repository selection (e.g., `assign-to-agent`), implementations MUST: +- Validate `pull-request-repo` format: `owner/repo` +- Validate against `allowed-pull-request-repos` allowlist when configured +- Use `target-repo` for the resource location (issue/PR) +- Use `pull-request-repo` for PR creation location +- Return E004 error code for unauthorized repositories + ### 3.5 Layer 4: Execution Handlers #### 3.5.1 Job Isolation @@ -704,6 +711,13 @@ Operations supporting cross-repository actions MUST: - Use target repository for all GitHub API calls - Apply same validation rules as same-repository operations +For operations that assign agents to issues/PRs, implementations MAY support: +- Accept `pull-request-repo: "owner/repo"` configuration to specify where PRs should be created +- Accept `allowed-pull-request-repos: ["owner/repo1", "owner/repo2"]` for validation +- Use `agentAssignment.targetRepositoryId` in GraphQL mutations when available + +This pattern enables issue tracking in one repository while code changes are created in a different repository. + Exceptions (same-repository only): - `push-to-pull-request-branch` - Requires repository write access - `upload-asset` - Creates orphaned branches @@ -951,7 +965,36 @@ safe-outputs: - Label `upstream-request` applied - Attribution footer references source workflow -#### A.3 Staged Mode Preview +#### A.3 Cross-Repository Agent Assignment + +**Configuration:** +```yaml +safe-outputs: + assign-to-agent: + target-repo: "octocat/issues" + pull-request-repo: "octocat/codebase" + allowed-pull-request-repos: + - "octocat/codebase" + - "octocat/codebase-v2" +``` + +**Agent Request:** +```json +{ + "type": "assign_to_agent", + "issue_number": 42, + "agent": "copilot", + "pull_request_repo": "octocat/codebase" +} +``` + +**Result:** +- Issue #42 in `octocat/issues` assigned to Copilot +- Agent creates PR in `octocat/codebase` (not in `octocat/issues`) +- GraphQL mutation includes `agentAssignment.targetRepositoryId` +- Enables issue tracking separate from code repositories + +#### A.4 Staged Mode Preview **Configuration:** ```yaml From 38069dd179f480ae0856bc6646789a55dcc5f330 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:23:02 +0000 Subject: [PATCH 7/7] Fix validation to auto-allow pull-request-repo like target-repo pattern Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 9 ++- actions/setup/js/assign_to_agent.test.cjs | 65 ++++++++++++++++++- .../content/docs/reference/safe-outputs.md | 2 + pkg/parser/schemas/main_workflow_schema.json | 2 +- pkg/workflow/assign_to_agent.go | 2 +- pkg/workflow/js/safe_outputs_tools.json | 2 +- scratchpad/safe-outputs-specification.md | 6 +- 7 files changed, 80 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 75329e9d624..a9c684739aa 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -134,7 +134,9 @@ async function main() { const parts = pullRequestRepoEnv.split("/"); if (parts.length === 2) { // Validate PR repository against allowlist - const repoValidation = validateRepo(pullRequestRepoEnv, defaultRepo, allowedPullRequestRepos); + // The configured pull-request-repo is treated as the default (always allowed) + // allowed-pull-request-repos contains additional repositories beyond pull-request-repo + const repoValidation = validateRepo(pullRequestRepoEnv, pullRequestRepoEnv, allowedPullRequestRepos); if (!repoValidation.valid) { core.setFailed(`E004: ${repoValidation.error}`); return; @@ -234,7 +236,10 @@ async function main() { const pullRequestRepoParts = itemPullRequestRepo.split("/"); if (pullRequestRepoParts.length === 2) { // Validate PR repository against allowlist - const pullRequestRepoValidation = validateRepo(itemPullRequestRepo, defaultRepo, allowedPullRequestRepos); + // The global pull-request-repo (if set) is treated as the default (always allowed) + // allowed-pull-request-repos contains additional allowed repositories + const defaultPullRequestRepo = pullRequestRepoEnv || defaultRepo; + const pullRequestRepoValidation = validateRepo(itemPullRequestRepo, defaultPullRequestRepo, allowedPullRequestRepos); if (!pullRequestRepoValidation.valid) { core.error(`E004: ${pullRequestRepoValidation.error}`); results.push({ diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index 5df426f9399..685c77b0bd6 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -1149,7 +1149,7 @@ describe("assign_to_agent", () => { it("should handle pull-request-repo configuration correctly", async () => { process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/pull-request-repo"; - process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS = "test-owner/pull-request-repo"; + // Note: pull-request-repo is automatically allowed, no need to set allowed list setAgentOutput({ items: [ { @@ -1206,6 +1206,9 @@ describe("assign_to_agent", () => { }); it("should handle per-item pull_request_repo parameter", async () => { + // Set global pull-request-repo which will be automatically allowed + process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/default-pr-repo"; + // Set allowed list for additional repos process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS = "test-owner/item-pull-request-repo"; setAgentOutput({ items: [ @@ -1221,6 +1224,12 @@ describe("assign_to_agent", () => { // Mock GraphQL responses mockGithub.graphql + // Get global PR repository ID (for default-pr-repo) + .mockResolvedValueOnce({ + repository: { + id: "default-pr-repo-id", + }, + }) // Get item PR repository ID .mockResolvedValueOnce({ repository: { @@ -1259,4 +1268,58 @@ describe("assign_to_agent", () => { const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; expect(lastGraphQLCall[1].targetRepoId).toBe("item-pull-request-repo-id"); }); + + it("should allow pull-request-repo without it being in allowed-pull-request-repos", async () => { + // Set pull-request-repo but DO NOT set allowed-pull-request-repos + // This tests that pull-request-repo is automatically allowed (like target-repo behavior) + process.env.GH_AW_AGENT_PULL_REQUEST_REPO = "test-owner/auto-allowed-repo"; + setAgentOutput({ + items: [ + { + type: "assign_to_agent", + issue_number: 42, + agent: "copilot", + }, + ], + errors: [], + }); + + // Mock GraphQL responses + mockGithub.graphql + // Get PR repository ID + .mockResolvedValueOnce({ + repository: { + id: "auto-allowed-repo-id", + }, + }) + // Find agent + .mockResolvedValueOnce({ + repository: { + suggestedActors: { + nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], + }, + }, + }) + // Get issue details + .mockResolvedValueOnce({ + repository: { + issue: { + id: "issue-id", + assignees: { nodes: [] }, + }, + }, + }) + // Assign agent with agentAssignment + .mockResolvedValueOnce({ + replaceActorsForAssignable: { + __typename: "ReplaceActorsForAssignablePayload", + }, + }); + + await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + + // Should succeed - pull-request-repo is automatically allowed + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using pull request repository: test-owner/auto-allowed-repo")); + }); }); diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index d1398f5a9a9..fe1863ae1d9 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -1321,6 +1321,8 @@ The `pull-request-repo` parameter allows you to create pull requests in a differ When `pull-request-repo` is configured, Copilot will create the pull request in the specified repository instead of the issue's repository. The issue repository is determined by `target-repo` or defaults to the workflow's repository. +The repository specified by `pull-request-repo` is automatically allowed - you don't need to list it in `allowed-pull-request-repos`. Use `allowed-pull-request-repos` to specify additional repositories where PRs can be created. + You can also specify `pull_request_repo` on a per-assignment basis in the agent output using the `assign_to_agent` tool: ```python assign_to_agent(issue_number=123, agent="copilot", pull_request_repo="owner/codebase-repo") diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 5616a5cb182..b5a7a65e026 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5389,7 +5389,7 @@ "items": { "type": "string" }, - "description": "List of additional repositories that pull requests can be created in (in addition to pull-request-repo). Each entry should be in 'owner/repo' format." + "description": "List of additional repositories that pull requests can be created in beyond pull-request-repo. Each entry should be in 'owner/repo' format. The repository specified by pull-request-repo is automatically allowed without needing to be listed here." }, "ignore-if-error": { "type": "boolean", diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index 84e1c2eed93..c86905a9a59 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -14,7 +14,7 @@ type AssignToAgentConfig struct { Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. IgnoreIfError bool `yaml:"ignore-if-error,omitempty"` // If true, workflow continues when agent assignment fails PullRequestRepoSlug string `yaml:"pull-request-repo,omitempty"` // Target repository for PR creation in format "owner/repo" (where the issue lives may differ) - AllowedPullRequestRepos []string `yaml:"allowed-pull-request-repos,omitempty"` // List of additional repositories that PRs can be created in (in addition to pull-request-repo) + AllowedPullRequestRepos []string `yaml:"allowed-pull-request-repos,omitempty"` // List of additional repositories that PRs can be created in (beyond pull-request-repo which is automatically allowed) } // parseAssignToAgentConfig handles assign-to-agent configuration diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 98918b0c565..dda35bd5881 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -528,7 +528,7 @@ }, "pull_request_repo": { "type": "string", - "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. Must be in allowed-pull-request-repos list if specified." + "description": "Target repository where the pull request should be created, in 'owner/repo' format. If omitted, the PR will be created in the same repository as the issue. This allows issues and code to live in different repositories. The global pull-request-repo configuration (if set) is automatically allowed; additional repositories must be listed in allowed-pull-request-repos." } }, "additionalProperties": false diff --git a/scratchpad/safe-outputs-specification.md b/scratchpad/safe-outputs-specification.md index 51ac3cd2f3c..018f750ef28 100644 --- a/scratchpad/safe-outputs-specification.md +++ b/scratchpad/safe-outputs-specification.md @@ -311,7 +311,8 @@ For cross-repository operations, implementations MUST: For operations that support separate PR repository selection (e.g., `assign-to-agent`), implementations MUST: - Validate `pull-request-repo` format: `owner/repo` -- Validate against `allowed-pull-request-repos` allowlist when configured +- Automatically allow the repository specified by `pull-request-repo` (no need to list in `allowed-pull-request-repos`) +- Validate per-item `pull_request_repo` values against the global `pull-request-repo` (default) and `allowed-pull-request-repos` (additional allowed repositories) - Use `target-repo` for the resource location (issue/PR) - Use `pull-request-repo` for PR creation location - Return E004 error code for unauthorized repositories @@ -713,7 +714,8 @@ Operations supporting cross-repository actions MUST: For operations that assign agents to issues/PRs, implementations MAY support: - Accept `pull-request-repo: "owner/repo"` configuration to specify where PRs should be created -- Accept `allowed-pull-request-repos: ["owner/repo1", "owner/repo2"]` for validation +- Accept `allowed-pull-request-repos: ["owner/repo1", "owner/repo2"]` for validation of additional repositories +- Automatically allow the repository specified by `pull-request-repo` (it does not need to be listed in `allowed-pull-request-repos`) - Use `agentAssignment.targetRepositoryId` in GraphQL mutations when available This pattern enables issue tracking in one repository while code changes are created in a different repository.