Skip to content

Commit 2f68c35

Browse files
Copilotpelikhan
andauthored
[WIP] Add support for multiple assignments to agent per issue (#28103)
* Initial plan * fix(assign_to_agent): allow repeated issue assignments in multi-repo flows Agent-Logs-Url: https://github.com/github/gh-aw/sessions/35aa1b6a-6d55-47e5-b040-3f7167d9bf21 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Changes before error encountered Agent-Logs-Url: https://github.com/github/gh-aw/sessions/35aa1b6a-6d55-47e5-b040-3f7167d9bf21 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * fix(assign_to_agent): address review feedback for multi-repo assignment Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d2b4077d-7102-41a5-930e-a274fbe9e182 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * test(assign_to_agent): remove real delays and harden per-item repo override handling Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d2b4077d-7102-41a5-930e-a274fbe9e182 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * refactor(assign_to_agent): clarify pull_request_repo override validation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d2b4077d-7102-41a5-930e-a274fbe9e182 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 94c3b13 commit 2f68c35

2 files changed

Lines changed: 262 additions & 13 deletions

File tree

actions/setup/js/assign_to_agent.cjs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const { sanitizeContent } = require("./sanitize_content.cjs");
1515
* Module-level state — populated by main(), read by the exported getters below.
1616
* Using module-level variables (rather than closure-only state) allows the handler
1717
* manager to read final output values after all messages have been processed.
18-
* @type {Array<{issue_number: number|null, pull_number: number|null, agent: string, owner: string|null, repo: string|null, success: boolean, skipped?: boolean, error?: string}>}
18+
* @type {Array<{issue_number: number|null, pull_number: number|null, agent: string, owner: string|null, repo: string|null, pull_request_repo?: string|null, success: boolean, skipped?: boolean, error?: string}>}
1919
*/
2020
let _allResults = [];
2121

@@ -128,6 +128,7 @@ async function main(config = {}) {
128128
// Closure-level state
129129
let processedCount = 0;
130130
const agentCache = {};
131+
const processedAssignmentTargets = new Set();
131132

132133
// Reset module-level results for this handler invocation
133134
_allResults = [];
@@ -232,10 +233,16 @@ async function main(config = {}) {
232233
const hasExplicitTarget = itemForTarget.issue_number != null || itemForTarget.pull_number != null;
233234
const effectiveTarget = hasExplicitTarget ? "*" : targetConfig;
234235

236+
const basePullRequestRepoSlug = pullRequestOwner && pullRequestRepo ? `${pullRequestOwner}/${pullRequestRepo}` : `${effectiveOwner}/${effectiveRepo}`;
237+
235238
// Handle per-item pull_request_repo override
236239
let effectivePullRequestRepoId = pullRequestRepoId;
237-
if (message.pull_request_repo) {
238-
const itemPullRequestRepo = String(message.pull_request_repo).trim();
240+
let effectivePullRequestRepoSlug = basePullRequestRepoSlug;
241+
let hasValidatedPerItemPullRequestRepoOverride = false;
242+
const hasPullRequestRepoOverrideField = message.pull_request_repo != null;
243+
const trimmedPullRequestRepoOverride = typeof message.pull_request_repo === "string" ? message.pull_request_repo.trim() : "";
244+
if (trimmedPullRequestRepoOverride) {
245+
const itemPullRequestRepo = trimmedPullRequestRepoOverride;
239246
const pullRequestRepoParts = itemPullRequestRepo.split("/");
240247
if (pullRequestRepoParts.length === 2) {
241248
const defaultPullRequestRepo = pullRequestRepoConfig || defaultTargetRepo;
@@ -254,6 +261,8 @@ async function main(config = {}) {
254261
`;
255262
const itemPullRequestRepoResponse = await githubClient.graphql(itemPullRequestRepoQuery, { owner: pullRequestRepoParts[0], name: pullRequestRepoParts[1] });
256263
effectivePullRequestRepoId = itemPullRequestRepoResponse.repository.id;
264+
effectivePullRequestRepoSlug = itemPullRequestRepo;
265+
hasValidatedPerItemPullRequestRepoOverride = true;
257266
core.info(`Using per-item pull request repository: ${itemPullRequestRepo} (ID: ${effectivePullRequestRepoId})`);
258267
} catch (error) {
259268
const errorMsg = `Failed to fetch pull request repository ID for ${itemPullRequestRepo}: ${getErrorMessage(error)}`;
@@ -264,6 +273,10 @@ async function main(config = {}) {
264273
} else {
265274
core.warning(`Invalid pull_request_repo format: ${itemPullRequestRepo}. Expected owner/repo. Using global pull-request-repo if configured.`);
266275
}
276+
} else if (hasPullRequestRepoOverrideField && typeof message.pull_request_repo === "string") {
277+
core.warning("Invalid pull_request_repo value. Expected owner/repo. Using global pull-request-repo if configured.");
278+
} else if (hasPullRequestRepoOverrideField) {
279+
core.warning("Invalid pull_request_repo value. Expected a non-empty owner/repo string. Using global pull-request-repo if configured.");
267280
}
268281

269282
// Resolve the target issue or pull request number from context
@@ -351,14 +364,19 @@ async function main(config = {}) {
351364

352365
core.info(`${type} ID: ${assignableId}`);
353366

354-
const hasPerItemPullRequestRepoOverride = !!message.pull_request_repo;
367+
const assignmentContextKey = `${effectiveOwner}/${effectiveRepo}:${type}:${number}:${effectivePullRequestRepoSlug}`;
368+
const seenThisContextBefore = processedAssignmentTargets.has(assignmentContextKey);
369+
// Track assignment context (target + per-item pull_request_repo) to prevent duplicate
370+
// re-assignment calls while still allowing one global issue to fan out to multiple repos.
371+
processedAssignmentTargets.add(assignmentContextKey);
372+
const shouldAllowReassignment = hasValidatedPerItemPullRequestRepoOverride && !seenThisContextBefore;
355373

356374
// Skip if agent is already assigned and no explicit per-item pull_request_repo is specified.
357375
// When a different pull_request_repo is provided on the message, allow re-assignment
358376
// so Copilot can be triggered for a different target repository on the same issue.
359-
if (currentAssignees.some(a => a.id === agentId) && !hasPerItemPullRequestRepoOverride) {
377+
if (currentAssignees.some(a => a.id === agentId) && !shouldAllowReassignment) {
360378
core.info(`${agentName} is already assigned to ${type} #${number}`);
361-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: true });
379+
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true });
362380
return { success: true };
363381
}
364382

@@ -372,7 +390,7 @@ async function main(config = {}) {
372390
if (!success) throw new Error(`Failed to assign ${agentName} via GraphQL`);
373391

374392
core.info(`Successfully assigned ${agentName} coding agent to ${type} #${number}`);
375-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: true });
393+
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true });
376394
return { success: true };
377395
} catch (error) {
378396
let errorMessage = getErrorMessage(error);
@@ -382,7 +400,7 @@ async function main(config = {}) {
382400
if (ignoreIfError && isAuthError) {
383401
core.warning(`Agent assignment failed for ${agentName} on ${type} #${number} due to authentication/permission error. Skipping due to ignore-if-error=true.`);
384402
core.info(`Error details: ${errorMessage}`);
385-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: true, skipped: true });
403+
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true, skipped: true });
386404
return { success: true, skipped: true };
387405
}
388406

@@ -410,7 +428,7 @@ async function main(config = {}) {
410428
core.warning(`Failed to post failure comment on ${type} #${number}: ${getErrorMessage(commentError)}`);
411429
}
412430

413-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: false, error: errorMessage });
431+
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: false, error: errorMessage });
414432
return { success: false, error: errorMessage };
415433
}
416434
};
@@ -475,7 +493,7 @@ async function writeAssignToAgentSummary() {
475493
summaryContent += successResults
476494
.map(r => {
477495
const itemType = r.issue_number ? `Issue #${r.issue_number}` : `Pull Request #${r.pull_number}`;
478-
return `- ${itemType} → Agent: ${r.agent}`;
496+
return `- ${itemType} → Agent: ${r.agent}${r.pull_request_repo ? ` (PR target: ${r.pull_request_repo})` : ""}`;
479497
})
480498
.join("\n");
481499
summaryContent += "\n\n";
@@ -486,7 +504,7 @@ async function writeAssignToAgentSummary() {
486504
summaryContent += skippedResults
487505
.map(r => {
488506
const itemType = r.issue_number ? `Issue #${r.issue_number}` : `Pull Request #${r.pull_number}`;
489-
return `- ${itemType} → Agent: ${r.agent} (assignment failed due to error)`;
507+
return `- ${itemType} → Agent: ${r.agent}${r.pull_request_repo ? ` (PR target: ${r.pull_request_repo})` : ""} (assignment failed due to error)`;
490508
})
491509
.join("\n");
492510
summaryContent += "\n\n";
@@ -497,7 +515,7 @@ async function writeAssignToAgentSummary() {
497515
summaryContent += failedResults
498516
.map(r => {
499517
const itemType = r.issue_number ? `Issue #${r.issue_number}` : `Pull Request #${r.pull_number}`;
500-
return `- ${itemType} → Agent: ${r.agent}: ${r.error}`;
518+
return `- ${itemType} → Agent: ${r.agent}${r.pull_request_repo ? ` (PR target: ${r.pull_request_repo})` : ""}: ${r.error}`;
501519
})
502520
.join("\n");
503521

0 commit comments

Comments
 (0)