From b19a98d084d37ba53fd88b5b6709322ee8e7e0e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:10:35 +0000 Subject: [PATCH 1/2] Initial plan From ac83fe35fa8f2328430b4d66b4d027bab7931ced Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:29:46 +0000 Subject: [PATCH 2/2] feat: Add REST API method support for assign_to_agent safe output Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- .github/workflows/ai-triage-campaign.lock.yml | 256 +++++++++++++----- .../breaking-change-checker.lock.yml | 70 +++++ .github/workflows/dev.lock.yml | 256 +++++++++++++----- .../duplicate-code-detector.lock.yml | 70 +++++ .github/workflows/issue-monster.lock.yml | 256 +++++++++++++----- .../docs/reference/frontmatter-full.md | 6 + pkg/parser/schemas/main_workflow_schema.json | 6 + pkg/workflow/assign_to_agent.go | 17 +- pkg/workflow/js/assign_agent_helpers.cjs | 111 ++++++++ pkg/workflow/js/assign_agent_helpers.test.cjs | 87 ++++++ pkg/workflow/js/assign_to_agent.cjs | 225 +++++++++------ 11 files changed, 1055 insertions(+), 305 deletions(-) diff --git a/.github/workflows/ai-triage-campaign.lock.yml b/.github/workflows/ai-triage-campaign.lock.yml index d5d71a58af..4da9ca9621 100644 --- a/.github/workflows/ai-triage-campaign.lock.yml +++ b/.github/workflows/ai-triage-campaign.lock.yml @@ -4986,6 +4986,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_AGENT_DEFAULT: "copilot" GH_AW_AGENT_MAX_COUNT: 1 + GH_AW_AGENT_API_METHOD: "graphql" GH_AW_WORKFLOW_NAME: "AI Triage Campaign" GH_AW_ENGINE_ID: "copilot" with: @@ -5424,6 +5425,76 @@ jobs: return { success: false, error: errorMessage }; } } + async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + const loginName = AGENT_LOGIN_NAMES[agentName]; + const assigneeLogin = `${loginName}[bot]`; + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + const agentAssignment = {}; + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + const requestBody = { + assignees: [assigneeLogin], + }; + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + return { success: false, error: errorMessage }; + } + } async function main() { const result = loadAgentOutput(); if (!result.success) { @@ -5435,6 +5506,8 @@ jobs: return; } core.info(`Found ${assignItems.length} assign_to_agent item(s)`); + const apiMethod = process.env.GH_AW_AGENT_API_METHOD?.trim() || "graphql"; + core.info(`API method: ${apiMethod}`); if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { await generateStagedPreview({ title: "Assign to Agent", @@ -5443,6 +5516,7 @@ jobs: renderItem: item => { let content = `**Issue:** #${item.issue_number}\n`; content += `**Agent:** ${item.agent || "copilot"}\n`; + content += `**API Method:** ${apiMethod}\n`; if (item.target_repository) { content += `**Target Repository:** ${item.target_repository}\n`; } @@ -5508,95 +5582,133 @@ jobs: }); continue; } - try { - let agentId = agentCache[agentName]; - if (!agentId) { - core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(targetOwner, targetRepo, agentName); - if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + const assignmentOptions = { + targetRepository: item.target_repository || null, + baseBranch: item.base_branch || null, + customInstructions: item.custom_instructions || null, + customAgent: item.custom_agent || null, + }; + if (apiMethod === "rest") { + try { + core.info(`Using REST API to assign ${agentName} to issue #${issueNumber}...`); + const restResult = await assignAgentViaRest(targetOwner, targetRepo, issueNumber, agentName, assignmentOptions); + if (restResult.success) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } else { + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${restResult.error}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: restResult.error, + }); } - agentCache[agentName] = agentId; - core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - } - core.info("Getting issue details..."); - const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); - if (!issueDetails) { - throw new Error("Failed to get issue details"); - } - core.info(`Issue ID: ${issueDetails.issueId}`); - if (issueDetails.currentAssignees.includes(agentId)) { - core.info(`${agentName} is already assigned to issue #${issueNumber}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); results.push({ issue_number: issueNumber, agent: agentName, - success: true, + success: false, + error: errorMessage, }); - continue; } - const assignmentOptions = {}; - const itemTargetRepo = item.target_repository; - if (itemTargetRepo) { - const parts = itemTargetRepo.split("/"); - if (parts.length === 2) { - const repoId = await getRepositoryId(parts[0], parts[1]); - if (repoId) { - assignmentOptions.targetRepositoryId = repoId; - core.info(`Target repository: ${itemTargetRepo} (ID: ${repoId})`); + } else { + try { + let agentId = agentCache[agentName]; + if (!agentId) { + core.info(`Looking for ${agentName} coding agent...`); + agentId = await findAgent(targetOwner, targetRepo, agentName); + if (!agentId) { + throw new Error(`${agentName} coding agent is not available for this repository`); + } + agentCache[agentName] = agentId; + core.info(`Found ${agentName} coding agent (ID: ${agentId})`); + } + core.info("Getting issue details..."); + const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); + if (!issueDetails) { + throw new Error("Failed to get issue details"); + } + core.info(`Issue ID: ${issueDetails.issueId}`); + if (issueDetails.currentAssignees.includes(agentId)) { + core.info(`${agentName} is already assigned to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + continue; + } + const graphqlOptions = {}; + if (assignmentOptions.targetRepository) { + const parts = assignmentOptions.targetRepository.split("/"); + if (parts.length === 2) { + const repoId = await getRepositoryId(parts[0], parts[1]); + if (repoId) { + graphqlOptions.targetRepositoryId = repoId; + core.info(`Target repository: ${assignmentOptions.targetRepository} (ID: ${repoId})`); + } else { + core.warning(`Could not find repository ID for ${assignmentOptions.targetRepository}`); + } } else { - core.warning(`Could not find repository ID for ${itemTargetRepo}`); + core.warning(`Invalid target_repository format: ${assignmentOptions.targetRepository}. Expected owner/repo.`); } - } else { - core.warning(`Invalid target_repository format: ${itemTargetRepo}. Expected owner/repo.`); } - } - if (item.base_branch) { - assignmentOptions.baseBranch = item.base_branch; - core.info(`Base branch: ${item.base_branch}`); - } - if (item.custom_instructions) { - assignmentOptions.customInstructions = item.custom_instructions; - core.info(`Custom instructions provided (${item.custom_instructions.length} characters)`); - } - if (item.custom_agent) { - assignmentOptions.customAgent = item.custom_agent; - core.info(`Custom agent: ${item.custom_agent}`); - } - core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, assignmentOptions); - if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); - } - core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: true, - }); - } catch (error) { - let errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("coding agent is not available for this repository")) { - try { - const available = await getAvailableAgentLogins(targetOwner, targetRepo); - if (available.length > 0) { - errorMessage += ` (available agents: ${available.join(", ")})`; + if (assignmentOptions.baseBranch) { + graphqlOptions.baseBranch = assignmentOptions.baseBranch; + core.info(`Base branch: ${assignmentOptions.baseBranch}`); + } + if (assignmentOptions.customInstructions) { + graphqlOptions.customInstructions = assignmentOptions.customInstructions; + core.info(`Custom instructions provided (${assignmentOptions.customInstructions.length} characters)`); + } + if (assignmentOptions.customAgent) { + graphqlOptions.customAgent = assignmentOptions.customAgent; + core.info(`Custom agent: ${assignmentOptions.customAgent}`); + } + core.info(`Assigning ${agentName} coding agent to issue #${issueNumber} via GraphQL...`); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, graphqlOptions); + if (!success) { + throw new Error(`Failed to assign ${agentName} via GraphQL`); + } + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("coding agent is not available for this repository")) { + try { + const available = await getAvailableAgentLogins(targetOwner, targetRepo); + if (available.length > 0) { + errorMessage += ` (available agents: ${available.join(", ")})`; + } + } catch (e) { + core.debug("Failed to enrich unavailable agent message with available list"); } - } catch (e) { - core.debug("Failed to enrich unavailable agent message with available list"); } + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: errorMessage, + }); } - core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: false, - error: errorMessage, - }); } } const successCount = results.filter(r => r.success).length; const failureCount = results.filter(r => !r.success).length; let summaryContent = "## Agent Assignment\n\n"; + summaryContent += `**API Method:** ${apiMethod}\n\n`; if (successCount > 0) { summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`; for (const result of results.filter(r => r.success)) { diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index 3fdf9197b2..eb457bc6a3 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -6628,6 +6628,76 @@ jobs: return { success: false, error: errorMessage }; } } + async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + const loginName = AGENT_LOGIN_NAMES[agentName]; + const assigneeLogin = `${loginName}[bot]`; + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + const agentAssignment = {}; + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + const requestBody = { + assignees: [assigneeLogin], + }; + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + return { success: false, error: errorMessage }; + } + } async function main() { const issuesToAssignStr = "${{ steps.create_issue.outputs.issues_to_assign_copilot }}"; if (!issuesToAssignStr || issuesToAssignStr.trim() === "") { diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 1daee11936..4742c3180d 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -4287,6 +4287,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_AGENT_DEFAULT: "copilot" GH_AW_AGENT_MAX_COUNT: 1 + GH_AW_AGENT_API_METHOD: "graphql" GH_AW_WORKFLOW_NAME: "Dev" GH_AW_ENGINE_ID: "claude" with: @@ -4725,6 +4726,76 @@ jobs: return { success: false, error: errorMessage }; } } + async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + const loginName = AGENT_LOGIN_NAMES[agentName]; + const assigneeLogin = `${loginName}[bot]`; + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + const agentAssignment = {}; + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + const requestBody = { + assignees: [assigneeLogin], + }; + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + return { success: false, error: errorMessage }; + } + } async function main() { const result = loadAgentOutput(); if (!result.success) { @@ -4736,6 +4807,8 @@ jobs: return; } core.info(`Found ${assignItems.length} assign_to_agent item(s)`); + const apiMethod = process.env.GH_AW_AGENT_API_METHOD?.trim() || "graphql"; + core.info(`API method: ${apiMethod}`); if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { await generateStagedPreview({ title: "Assign to Agent", @@ -4744,6 +4817,7 @@ jobs: renderItem: item => { let content = `**Issue:** #${item.issue_number}\n`; content += `**Agent:** ${item.agent || "copilot"}\n`; + content += `**API Method:** ${apiMethod}\n`; if (item.target_repository) { content += `**Target Repository:** ${item.target_repository}\n`; } @@ -4809,95 +4883,133 @@ jobs: }); continue; } - try { - let agentId = agentCache[agentName]; - if (!agentId) { - core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(targetOwner, targetRepo, agentName); - if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + const assignmentOptions = { + targetRepository: item.target_repository || null, + baseBranch: item.base_branch || null, + customInstructions: item.custom_instructions || null, + customAgent: item.custom_agent || null, + }; + if (apiMethod === "rest") { + try { + core.info(`Using REST API to assign ${agentName} to issue #${issueNumber}...`); + const restResult = await assignAgentViaRest(targetOwner, targetRepo, issueNumber, agentName, assignmentOptions); + if (restResult.success) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } else { + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${restResult.error}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: restResult.error, + }); } - agentCache[agentName] = agentId; - core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - } - core.info("Getting issue details..."); - const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); - if (!issueDetails) { - throw new Error("Failed to get issue details"); - } - core.info(`Issue ID: ${issueDetails.issueId}`); - if (issueDetails.currentAssignees.includes(agentId)) { - core.info(`${agentName} is already assigned to issue #${issueNumber}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); results.push({ issue_number: issueNumber, agent: agentName, - success: true, + success: false, + error: errorMessage, }); - continue; } - const assignmentOptions = {}; - const itemTargetRepo = item.target_repository; - if (itemTargetRepo) { - const parts = itemTargetRepo.split("/"); - if (parts.length === 2) { - const repoId = await getRepositoryId(parts[0], parts[1]); - if (repoId) { - assignmentOptions.targetRepositoryId = repoId; - core.info(`Target repository: ${itemTargetRepo} (ID: ${repoId})`); + } else { + try { + let agentId = agentCache[agentName]; + if (!agentId) { + core.info(`Looking for ${agentName} coding agent...`); + agentId = await findAgent(targetOwner, targetRepo, agentName); + if (!agentId) { + throw new Error(`${agentName} coding agent is not available for this repository`); + } + agentCache[agentName] = agentId; + core.info(`Found ${agentName} coding agent (ID: ${agentId})`); + } + core.info("Getting issue details..."); + const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); + if (!issueDetails) { + throw new Error("Failed to get issue details"); + } + core.info(`Issue ID: ${issueDetails.issueId}`); + if (issueDetails.currentAssignees.includes(agentId)) { + core.info(`${agentName} is already assigned to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + continue; + } + const graphqlOptions = {}; + if (assignmentOptions.targetRepository) { + const parts = assignmentOptions.targetRepository.split("/"); + if (parts.length === 2) { + const repoId = await getRepositoryId(parts[0], parts[1]); + if (repoId) { + graphqlOptions.targetRepositoryId = repoId; + core.info(`Target repository: ${assignmentOptions.targetRepository} (ID: ${repoId})`); + } else { + core.warning(`Could not find repository ID for ${assignmentOptions.targetRepository}`); + } } else { - core.warning(`Could not find repository ID for ${itemTargetRepo}`); + core.warning(`Invalid target_repository format: ${assignmentOptions.targetRepository}. Expected owner/repo.`); } - } else { - core.warning(`Invalid target_repository format: ${itemTargetRepo}. Expected owner/repo.`); } - } - if (item.base_branch) { - assignmentOptions.baseBranch = item.base_branch; - core.info(`Base branch: ${item.base_branch}`); - } - if (item.custom_instructions) { - assignmentOptions.customInstructions = item.custom_instructions; - core.info(`Custom instructions provided (${item.custom_instructions.length} characters)`); - } - if (item.custom_agent) { - assignmentOptions.customAgent = item.custom_agent; - core.info(`Custom agent: ${item.custom_agent}`); - } - core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, assignmentOptions); - if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); - } - core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: true, - }); - } catch (error) { - let errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("coding agent is not available for this repository")) { - try { - const available = await getAvailableAgentLogins(targetOwner, targetRepo); - if (available.length > 0) { - errorMessage += ` (available agents: ${available.join(", ")})`; + if (assignmentOptions.baseBranch) { + graphqlOptions.baseBranch = assignmentOptions.baseBranch; + core.info(`Base branch: ${assignmentOptions.baseBranch}`); + } + if (assignmentOptions.customInstructions) { + graphqlOptions.customInstructions = assignmentOptions.customInstructions; + core.info(`Custom instructions provided (${assignmentOptions.customInstructions.length} characters)`); + } + if (assignmentOptions.customAgent) { + graphqlOptions.customAgent = assignmentOptions.customAgent; + core.info(`Custom agent: ${assignmentOptions.customAgent}`); + } + core.info(`Assigning ${agentName} coding agent to issue #${issueNumber} via GraphQL...`); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, graphqlOptions); + if (!success) { + throw new Error(`Failed to assign ${agentName} via GraphQL`); + } + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("coding agent is not available for this repository")) { + try { + const available = await getAvailableAgentLogins(targetOwner, targetRepo); + if (available.length > 0) { + errorMessage += ` (available agents: ${available.join(", ")})`; + } + } catch (e) { + core.debug("Failed to enrich unavailable agent message with available list"); } - } catch (e) { - core.debug("Failed to enrich unavailable agent message with available list"); } + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: errorMessage, + }); } - core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: false, - error: errorMessage, - }); } } const successCount = results.filter(r => r.success).length; const failureCount = results.filter(r => !r.success).length; let summaryContent = "## Agent Assignment\n\n"; + summaryContent += `**API Method:** ${apiMethod}\n\n`; if (successCount > 0) { summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`; for (const result of results.filter(r => r.success)) { diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index b31dcde78e..47f5944ded 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -6189,6 +6189,76 @@ jobs: return { success: false, error: errorMessage }; } } + async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + const loginName = AGENT_LOGIN_NAMES[agentName]; + const assigneeLogin = `${loginName}[bot]`; + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + const agentAssignment = {}; + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + const requestBody = { + assignees: [assigneeLogin], + }; + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + return { success: false, error: errorMessage }; + } + } async function main() { const issuesToAssignStr = "${{ steps.create_issue.outputs.issues_to_assign_copilot }}"; if (!issuesToAssignStr || issuesToAssignStr.trim() === "") { diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index 9da8440390..25ddb14fef 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -5875,6 +5875,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_AGENT_DEFAULT: "copilot" GH_AW_AGENT_MAX_COUNT: 3 + GH_AW_AGENT_API_METHOD: "graphql" GH_AW_WORKFLOW_NAME: "Issue Monster" GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 🍪 *Om nom nom by [{workflow_name}]({run_url})*\",\"runStarted\":\"🍪 ISSUE! ISSUE! [{workflow_name}]({run_url}) hungry for issues on this {event_type}! Om nom nom...\",\"runSuccess\":\"🍪 YUMMY! [{workflow_name}]({run_url}) ate the issues! That was DELICIOUS! Me want MORE! 😋\",\"runFailure\":\"🍪 Aww... [{workflow_name}]({run_url}) {status}. No cookie for monster today... 😢\"}" @@ -6314,6 +6315,76 @@ jobs: return { success: false, error: errorMessage }; } } + async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + const loginName = AGENT_LOGIN_NAMES[agentName]; + const assigneeLogin = `${loginName}[bot]`; + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + const agentAssignment = {}; + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + const requestBody = { + assignees: [assigneeLogin], + }; + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + return { success: false, error: errorMessage }; + } + } async function main() { const result = loadAgentOutput(); if (!result.success) { @@ -6325,6 +6396,8 @@ jobs: return; } core.info(`Found ${assignItems.length} assign_to_agent item(s)`); + const apiMethod = process.env.GH_AW_AGENT_API_METHOD?.trim() || "graphql"; + core.info(`API method: ${apiMethod}`); if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { await generateStagedPreview({ title: "Assign to Agent", @@ -6333,6 +6406,7 @@ jobs: renderItem: item => { let content = `**Issue:** #${item.issue_number}\n`; content += `**Agent:** ${item.agent || "copilot"}\n`; + content += `**API Method:** ${apiMethod}\n`; if (item.target_repository) { content += `**Target Repository:** ${item.target_repository}\n`; } @@ -6398,95 +6472,133 @@ jobs: }); continue; } - try { - let agentId = agentCache[agentName]; - if (!agentId) { - core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(targetOwner, targetRepo, agentName); - if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + const assignmentOptions = { + targetRepository: item.target_repository || null, + baseBranch: item.base_branch || null, + customInstructions: item.custom_instructions || null, + customAgent: item.custom_agent || null, + }; + if (apiMethod === "rest") { + try { + core.info(`Using REST API to assign ${agentName} to issue #${issueNumber}...`); + const restResult = await assignAgentViaRest(targetOwner, targetRepo, issueNumber, agentName, assignmentOptions); + if (restResult.success) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } else { + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${restResult.error}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: restResult.error, + }); } - agentCache[agentName] = agentId; - core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - } - core.info("Getting issue details..."); - const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); - if (!issueDetails) { - throw new Error("Failed to get issue details"); - } - core.info(`Issue ID: ${issueDetails.issueId}`); - if (issueDetails.currentAssignees.includes(agentId)) { - core.info(`${agentName} is already assigned to issue #${issueNumber}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); results.push({ issue_number: issueNumber, agent: agentName, - success: true, + success: false, + error: errorMessage, }); - continue; } - const assignmentOptions = {}; - const itemTargetRepo = item.target_repository; - if (itemTargetRepo) { - const parts = itemTargetRepo.split("/"); - if (parts.length === 2) { - const repoId = await getRepositoryId(parts[0], parts[1]); - if (repoId) { - assignmentOptions.targetRepositoryId = repoId; - core.info(`Target repository: ${itemTargetRepo} (ID: ${repoId})`); + } else { + try { + let agentId = agentCache[agentName]; + if (!agentId) { + core.info(`Looking for ${agentName} coding agent...`); + agentId = await findAgent(targetOwner, targetRepo, agentName); + if (!agentId) { + throw new Error(`${agentName} coding agent is not available for this repository`); + } + agentCache[agentName] = agentId; + core.info(`Found ${agentName} coding agent (ID: ${agentId})`); + } + core.info("Getting issue details..."); + const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); + if (!issueDetails) { + throw new Error("Failed to get issue details"); + } + core.info(`Issue ID: ${issueDetails.issueId}`); + if (issueDetails.currentAssignees.includes(agentId)) { + core.info(`${agentName} is already assigned to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + continue; + } + const graphqlOptions = {}; + if (assignmentOptions.targetRepository) { + const parts = assignmentOptions.targetRepository.split("/"); + if (parts.length === 2) { + const repoId = await getRepositoryId(parts[0], parts[1]); + if (repoId) { + graphqlOptions.targetRepositoryId = repoId; + core.info(`Target repository: ${assignmentOptions.targetRepository} (ID: ${repoId})`); + } else { + core.warning(`Could not find repository ID for ${assignmentOptions.targetRepository}`); + } } else { - core.warning(`Could not find repository ID for ${itemTargetRepo}`); + core.warning(`Invalid target_repository format: ${assignmentOptions.targetRepository}. Expected owner/repo.`); } - } else { - core.warning(`Invalid target_repository format: ${itemTargetRepo}. Expected owner/repo.`); } - } - if (item.base_branch) { - assignmentOptions.baseBranch = item.base_branch; - core.info(`Base branch: ${item.base_branch}`); - } - if (item.custom_instructions) { - assignmentOptions.customInstructions = item.custom_instructions; - core.info(`Custom instructions provided (${item.custom_instructions.length} characters)`); - } - if (item.custom_agent) { - assignmentOptions.customAgent = item.custom_agent; - core.info(`Custom agent: ${item.custom_agent}`); - } - core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, assignmentOptions); - if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); - } - core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: true, - }); - } catch (error) { - let errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("coding agent is not available for this repository")) { - try { - const available = await getAvailableAgentLogins(targetOwner, targetRepo); - if (available.length > 0) { - errorMessage += ` (available agents: ${available.join(", ")})`; + if (assignmentOptions.baseBranch) { + graphqlOptions.baseBranch = assignmentOptions.baseBranch; + core.info(`Base branch: ${assignmentOptions.baseBranch}`); + } + if (assignmentOptions.customInstructions) { + graphqlOptions.customInstructions = assignmentOptions.customInstructions; + core.info(`Custom instructions provided (${assignmentOptions.customInstructions.length} characters)`); + } + if (assignmentOptions.customAgent) { + graphqlOptions.customAgent = assignmentOptions.customAgent; + core.info(`Custom agent: ${assignmentOptions.customAgent}`); + } + core.info(`Assigning ${agentName} coding agent to issue #${issueNumber} via GraphQL...`); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, graphqlOptions); + if (!success) { + throw new Error(`Failed to assign ${agentName} via GraphQL`); + } + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("coding agent is not available for this repository")) { + try { + const available = await getAvailableAgentLogins(targetOwner, targetRepo); + if (available.length > 0) { + errorMessage += ` (available agents: ${available.join(", ")})`; + } + } catch (e) { + core.debug("Failed to enrich unavailable agent message with available list"); } - } catch (e) { - core.debug("Failed to enrich unavailable agent message with available list"); } + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: errorMessage, + }); } - core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: false, - error: errorMessage, - }); } } const successCount = results.filter(r => r.success).length; const failureCount = results.filter(r => !r.success).length; let summaryContent = "## Agent Assignment\n\n"; + summaryContent += `**API Method:** ${apiMethod}\n\n`; if (successCount > 0) { summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`; for (const result of results.filter(r => r.success)) { diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index c9e8731682..97970c35a5 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -2093,6 +2093,12 @@ safe-outputs: # (optional) target-repo: "example-value" + # API method to use for assignment: 'graphql' (default) uses GraphQL mutation, + # 'rest' uses REST API endpoints. REST API supports the agent_assignment object + # with target_repo, base_branch, custom_instructions, and custom_agent fields. + # (optional) + api-method: "graphql" + # GitHub token to use for this specific output type. Overrides global github-token # if specified. # (optional) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 0c3a0406bd..25ceab6584 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3523,6 +3523,12 @@ "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository agent assignment. Takes precedence over trial target repo settings." }, + "api-method": { + "type": "string", + "description": "API method to use for assignment: 'graphql' (default) uses GraphQL mutation, 'rest' uses REST API endpoints. REST API supports the agent_assignment object with target_repo, base_branch, custom_instructions, and custom_agent fields.", + "enum": ["graphql", "rest"], + "default": "graphql" + }, "github-token": { "$ref": "#/$defs/github_token", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index 7089e46661..3e5e31fc07 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -8,7 +8,8 @@ import ( type AssignToAgentConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` - DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") + DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") + APIMethod string `yaml:"api-method,omitempty"` // API method to use: "graphql" (default) or "rest" } // parseAssignToAgentConfig handles assign-to-agent configuration @@ -24,6 +25,13 @@ func (c *Compiler) parseAssignToAgentConfig(outputMap map[string]any) *AssignToA } } + // Parse api-method (optional - specific to assign-to-agent) + if apiMethod, exists := agentMap["api-method"]; exists { + if apiMethodStr, ok := apiMethod.(string); ok { + agentConfig.APIMethod = apiMethodStr + } + } + // Parse target config (target, target-repo) - validation errors are handled gracefully targetConfig, _ := ParseTargetConfig(agentMap) agentConfig.SafeOutputTargetConfig = targetConfig @@ -52,6 +60,7 @@ func (c *Compiler) buildAssignToAgentJob(data *WorkflowData, mainJobName string) // Handle case where AssignToAgent is not nil defaultAgent := "copilot" maxCount := 1 + apiMethod := "graphql" // Default to graphql if cfg.DefaultAgent != "" { defaultAgent = cfg.DefaultAgent @@ -59,6 +68,9 @@ func (c *Compiler) buildAssignToAgentJob(data *WorkflowData, mainJobName string) if cfg.Max > 0 { maxCount = cfg.Max } + if cfg.APIMethod != "" { + apiMethod = cfg.APIMethod + } // Build custom environment variables specific to assign-to-agent var customEnvVars []string @@ -69,6 +81,9 @@ func (c *Compiler) buildAssignToAgentJob(data *WorkflowData, mainJobName string) // Pass the max limit customEnvVars = append(customEnvVars, BuildMaxCountEnvVar("GH_AW_AGENT_MAX_COUNT", maxCount)...) + // Pass the API method + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_API_METHOD: %q\n", apiMethod)) + // Add standard environment variables (metadata + staged/target repo) customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, cfg.TargetRepoSlug)...) diff --git a/pkg/workflow/js/assign_agent_helpers.cjs b/pkg/workflow/js/assign_agent_helpers.cjs index a3bf5e8486..86790511a4 100644 --- a/pkg/workflow/js/assign_agent_helpers.cjs +++ b/pkg/workflow/js/assign_agent_helpers.cjs @@ -527,6 +527,116 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName, opt } } +/** + * Assign an agent to an issue using REST API + * This uses the REST API endpoints announced in December 2025 + * @param {string} owner - Repository owner + * @param {string} repo - Repository name + * @param {number} issueNumber - Issue number + * @param {string} agentName - Agent name (e.g., "copilot") + * @param {object} options - Optional assignment options + * @param {string} [options.targetRepository] - Target repository in 'owner/repo' format + * @param {string} [options.baseBranch] - Base branch for the PR + * @param {string} [options.customInstructions] - Custom instructions for the agent + * @param {string} [options.customAgent] - Custom agent name/path + * @returns {Promise<{success: boolean, error?: string}>} + */ +async function assignAgentViaRest(owner, repo, issueNumber, agentName, options = {}) { + // Check if agent is supported + if (!AGENT_LOGIN_NAMES[agentName]) { + const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + + const loginName = AGENT_LOGIN_NAMES[agentName]; + // REST API uses the bot login name with [bot] suffix + const assigneeLogin = `${loginName}[bot]`; + + try { + core.info(`Assigning ${agentName} via REST API to issue #${issueNumber}...`); + + // Build agent_assignment object for REST API + const agentAssignment = {}; + + if (options.targetRepository) { + agentAssignment.target_repo = options.targetRepository; + } + + if (options.baseBranch) { + agentAssignment.base_branch = options.baseBranch; + } + + if (options.customInstructions) { + agentAssignment.custom_instructions = options.customInstructions; + } + + if (options.customAgent) { + agentAssignment.custom_agent = options.customAgent; + } + + // Build request body + const requestBody = { + assignees: [assigneeLogin], + }; + + // Only include agent_assignment if we have options + if (Object.keys(agentAssignment).length > 0) { + requestBody.agent_assignment = agentAssignment; + core.info(`Using agent_assignment options: ${JSON.stringify(agentAssignment)}`); + } + + core.debug(`REST API request body: ${JSON.stringify(requestBody)}`); + + // Use the REST API to add assignees + // POST /repos/{owner}/{repo}/issues/{issue_number}/assignees + const response = await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: issueNumber, + ...requestBody, + }); + + if (response.status === 201 || response.status === 200) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + return { success: true }; + } else { + const error = `Unexpected response status: ${response.status}`; + core.error(error); + return { success: false, error }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // Check for common REST API errors + if (errorMessage.includes("422") || errorMessage.includes("Unprocessable Entity") || errorMessage.includes("Invalid assignees")) { + core.error(`REST API assignment failed: ${errorMessage}`); + core.error(`This may occur if the agent (${assigneeLogin}) is not available for this repository.`); + core.error("Try using api-method: graphql instead, or verify Copilot is enabled for this repository."); + + // Try to enrich error with available agents + try { + const available = await getAvailableAgentLogins(owner, repo); + if (available.length > 0) { + core.info(`Available agents via GraphQL: ${available.join(", ")}`); + } + } catch { + // Ignore enrichment errors + } + } else if ( + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("403") + ) { + logPermissionError(agentName); + } else { + core.error(`Failed to assign ${agentName} via REST API: ${errorMessage}`); + } + + return { success: false, error: errorMessage }; + } +} + module.exports = { AGENT_LOGIN_NAMES, getAgentName, @@ -538,4 +648,5 @@ module.exports = { logPermissionError, generatePermissionErrorSummary, assignAgentToIssueByName, + assignAgentViaRest, }; diff --git a/pkg/workflow/js/assign_agent_helpers.test.cjs b/pkg/workflow/js/assign_agent_helpers.test.cjs index 125d463128..d83d7c2d27 100644 --- a/pkg/workflow/js/assign_agent_helpers.test.cjs +++ b/pkg/workflow/js/assign_agent_helpers.test.cjs @@ -26,6 +26,7 @@ const { assignAgentToIssue, generatePermissionErrorSummary, assignAgentToIssueByName, + assignAgentViaRest, } = await import("./assign_agent_helpers.cjs"); describe("assign_agent_helpers.cjs", () => { @@ -445,4 +446,90 @@ describe("assign_agent_helpers.cjs", () => { expect(mockCore.info).toHaveBeenCalledWith("copilot is already assigned to issue #123"); }); }); + + describe("assignAgentViaRest", () => { + it("should successfully assign agent via REST API", async () => { + // Mock the REST API call + github.rest = { + issues: { + addAssignees: vi.fn().mockResolvedValueOnce({ status: 201 }), + }, + }; + + const result = await assignAgentViaRest("owner", "repo", 123, "copilot"); + + expect(result.success).toBe(true); + expect(github.rest.issues.addAssignees).toHaveBeenCalledWith({ + owner: "owner", + repo: "repo", + issue_number: 123, + assignees: ["copilot-swe-agent[bot]"], + }); + }); + + it("should include agent_assignment options when provided", async () => { + github.rest = { + issues: { + addAssignees: vi.fn().mockResolvedValueOnce({ status: 201 }), + }, + }; + + const options = { + targetRepository: "other-owner/other-repo", + baseBranch: "develop", + customInstructions: "Test instructions", + customAgent: "my-agent", + }; + + const result = await assignAgentViaRest("owner", "repo", 123, "copilot", options); + + expect(result.success).toBe(true); + expect(github.rest.issues.addAssignees).toHaveBeenCalledWith({ + owner: "owner", + repo: "repo", + issue_number: 123, + assignees: ["copilot-swe-agent[bot]"], + agent_assignment: { + target_repo: "other-owner/other-repo", + base_branch: "develop", + custom_instructions: "Test instructions", + custom_agent: "my-agent", + }, + }); + }); + + it("should return error for unsupported agent", async () => { + const result = await assignAgentViaRest("owner", "repo", 123, "unknown-agent"); + + expect(result.success).toBe(false); + expect(result.error).toContain("not supported"); + }); + + it("should handle REST API 422 errors", async () => { + github.rest = { + issues: { + addAssignees: vi.fn().mockRejectedValueOnce(new Error("422 Unprocessable Entity: Invalid assignees")), + }, + }; + + const result = await assignAgentViaRest("owner", "repo", 123, "copilot"); + + expect(result.success).toBe(false); + expect(result.error).toContain("422"); + expect(mockCore.error).toHaveBeenCalled(); + }); + + it("should handle permission errors", async () => { + github.rest = { + issues: { + addAssignees: vi.fn().mockRejectedValueOnce(new Error("403 Resource not accessible")), + }, + }; + + const result = await assignAgentViaRest("owner", "repo", 123, "copilot"); + + expect(result.success).toBe(false); + expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Insufficient permissions")); + }); + }); }); diff --git a/pkg/workflow/js/assign_to_agent.cjs b/pkg/workflow/js/assign_to_agent.cjs index d285ae00da..b5b66c841c 100644 --- a/pkg/workflow/js/assign_to_agent.cjs +++ b/pkg/workflow/js/assign_to_agent.cjs @@ -11,6 +11,7 @@ const { getIssueDetails, assignAgentToIssue, generatePermissionErrorSummary, + assignAgentViaRest, } = require("./assign_agent_helpers.cjs"); async function main() { @@ -27,6 +28,10 @@ async function main() { core.info(`Found ${assignItems.length} assign_to_agent item(s)`); + // Get API method configuration (default: graphql) + const apiMethod = process.env.GH_AW_AGENT_API_METHOD?.trim() || "graphql"; + core.info(`API method: ${apiMethod}`); + // Check if we're in staged mode if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { await generateStagedPreview({ @@ -36,6 +41,7 @@ async function main() { renderItem: item => { let content = `**Issue:** #${item.issue_number}\n`; content += `**Agent:** ${item.agent || "copilot"}\n`; + content += `**API Method:** ${apiMethod}\n`; if (item.target_repository) { content += `**Target Repository:** ${item.target_repository}\n`; } @@ -95,7 +101,7 @@ 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) - // Cache agent IDs to avoid repeated lookups + // Cache agent IDs to avoid repeated lookups (only used for GraphQL) const agentCache = {}; // Process each agent assignment @@ -121,112 +127,154 @@ async function main() { continue; } - // Assign the agent to the issue using GraphQL - try { - // Find agent (use cache if available) - uses built-in github object authenticated via github-token - let agentId = agentCache[agentName]; - if (!agentId) { - core.info(`Looking for ${agentName} coding agent...`); - agentId = await findAgent(targetOwner, targetRepo, agentName); - if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + // Prepare assignment options (used by both REST and GraphQL) + const assignmentOptions = { + targetRepository: item.target_repository || null, + baseBranch: item.base_branch || null, + customInstructions: item.custom_instructions || null, + customAgent: item.custom_agent || null, + }; + + // Use REST or GraphQL based on configuration + if (apiMethod === "rest") { + // REST API assignment + try { + core.info(`Using REST API to assign ${agentName} to issue #${issueNumber}...`); + const restResult = await assignAgentViaRest(targetOwner, targetRepo, issueNumber, agentName, assignmentOptions); + + if (restResult.success) { + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber} via REST API`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } else { + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${restResult.error}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: restResult.error, + }); } - agentCache[agentName] = agentId; - core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - } - - // Get issue details (ID and current assignees) via GraphQL - core.info("Getting issue details..."); - const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); - if (!issueDetails) { - throw new Error("Failed to get issue details"); - } - - core.info(`Issue ID: ${issueDetails.issueId}`); - - // Check if agent is already assigned - if (issueDetails.currentAssignees.includes(agentId)) { - core.info(`${agentName} is already assigned to issue #${issueNumber}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); results.push({ issue_number: issueNumber, agent: agentName, - success: true, + success: false, + error: errorMessage, }); - continue; } + } else { + // GraphQL assignment (default) + try { + // Find agent (use cache if available) - uses built-in github object authenticated via github-token + let agentId = agentCache[agentName]; + if (!agentId) { + core.info(`Looking for ${agentName} coding agent...`); + agentId = await findAgent(targetOwner, targetRepo, agentName); + if (!agentId) { + throw new Error(`${agentName} coding agent is not available for this repository`); + } + agentCache[agentName] = agentId; + core.info(`Found ${agentName} coding agent (ID: ${agentId})`); + } + + // Get issue details (ID and current assignees) via GraphQL + core.info("Getting issue details..."); + const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); + if (!issueDetails) { + throw new Error("Failed to get issue details"); + } - // Prepare assignment options - const assignmentOptions = {}; - - // Handle target repository if specified (either from item or environment) - const itemTargetRepo = item.target_repository; - if (itemTargetRepo) { - const parts = itemTargetRepo.split("/"); - if (parts.length === 2) { - const repoId = await getRepositoryId(parts[0], parts[1]); - if (repoId) { - assignmentOptions.targetRepositoryId = repoId; - core.info(`Target repository: ${itemTargetRepo} (ID: ${repoId})`); + core.info(`Issue ID: ${issueDetails.issueId}`); + + // Check if agent is already assigned + if (issueDetails.currentAssignees.includes(agentId)) { + core.info(`${agentName} is already assigned to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + continue; + } + + // Prepare GraphQL-specific assignment options + const graphqlOptions = {}; + + // Handle target repository if specified (needs to be resolved to ID for GraphQL) + if (assignmentOptions.targetRepository) { + const parts = assignmentOptions.targetRepository.split("/"); + if (parts.length === 2) { + const repoId = await getRepositoryId(parts[0], parts[1]); + if (repoId) { + graphqlOptions.targetRepositoryId = repoId; + core.info(`Target repository: ${assignmentOptions.targetRepository} (ID: ${repoId})`); + } else { + core.warning(`Could not find repository ID for ${assignmentOptions.targetRepository}`); + } } else { - core.warning(`Could not find repository ID for ${itemTargetRepo}`); + core.warning(`Invalid target_repository format: ${assignmentOptions.targetRepository}. Expected owner/repo.`); } - } else { - core.warning(`Invalid target_repository format: ${itemTargetRepo}. Expected owner/repo.`); } - } - // Handle base branch - if (item.base_branch) { - assignmentOptions.baseBranch = item.base_branch; - core.info(`Base branch: ${item.base_branch}`); - } + // Handle base branch + if (assignmentOptions.baseBranch) { + graphqlOptions.baseBranch = assignmentOptions.baseBranch; + core.info(`Base branch: ${assignmentOptions.baseBranch}`); + } - // Handle custom instructions - if (item.custom_instructions) { - assignmentOptions.customInstructions = item.custom_instructions; - core.info(`Custom instructions provided (${item.custom_instructions.length} characters)`); - } + // Handle custom instructions + if (assignmentOptions.customInstructions) { + graphqlOptions.customInstructions = assignmentOptions.customInstructions; + core.info(`Custom instructions provided (${assignmentOptions.customInstructions.length} characters)`); + } - // Handle custom agent - if (item.custom_agent) { - assignmentOptions.customAgent = item.custom_agent; - core.info(`Custom agent: ${item.custom_agent}`); - } + // Handle custom agent + if (assignmentOptions.customAgent) { + graphqlOptions.customAgent = assignmentOptions.customAgent; + core.info(`Custom agent: ${assignmentOptions.customAgent}`); + } - // Assign agent using GraphQL mutation - uses built-in github object authenticated via github-token - core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, assignmentOptions); + // Assign agent using GraphQL mutation - uses built-in github object authenticated via github-token + core.info(`Assigning ${agentName} coding agent to issue #${issueNumber} via GraphQL...`); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, graphqlOptions); - if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); - } + if (!success) { + throw new Error(`Failed to assign ${agentName} via GraphQL`); + } - core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: true, - }); - } catch (error) { - let errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("coding agent is not available for this repository")) { - // Enrich with available agent logins to aid troubleshooting - uses built-in github object - try { - const available = await getAvailableAgentLogins(targetOwner, targetRepo); - if (available.length > 0) { - errorMessage += ` (available agents: ${available.join(", ")})`; + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("coding agent is not available for this repository")) { + // Enrich with available agent logins to aid troubleshooting - uses built-in github object + try { + const available = await getAvailableAgentLogins(targetOwner, targetRepo); + if (available.length > 0) { + errorMessage += ` (available agents: ${available.join(", ")})`; + } + } catch (e) { + core.debug("Failed to enrich unavailable agent message with available list"); } - } catch (e) { - core.debug("Failed to enrich unavailable agent message with available list"); } + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: errorMessage, + }); } - core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); - results.push({ - issue_number: issueNumber, - agent: agentName, - success: false, - error: errorMessage, - }); } } @@ -235,6 +283,7 @@ async function main() { const failureCount = results.filter(r => !r.success).length; let summaryContent = "## Agent Assignment\n\n"; + summaryContent += `**API Method:** ${apiMethod}\n\n`; if (successCount > 0) { summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`;