From 30426efce0d36b4d7b8247d9f82966291f15e68c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:08:18 +0000 Subject: [PATCH 1/9] refactor: use REST API for assign-to-agent task creation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 641 +++++++++++----------- actions/setup/js/assign_to_agent.cjs | 15 +- actions/setup/js/pr_helpers.cjs | 32 +- 3 files changed, 338 insertions(+), 350 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 808a88120b0..d979ad1e097 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -4,12 +4,8 @@ const { getErrorMessage } = require("./error_helpers.cjs"); /** - * Shared helper functions for assigning coding agents (like Copilot) to issues - * These functions use GraphQL to properly assign bot actors that cannot be assigned via gh CLI - * - * NOTE: All functions use the built-in `github` global object for authentication. - * The token must be set at the step level via the `github-token` parameter in GitHub Actions. - * This approach is required for compatibility with actions/github-script@v9. + * Shared helper functions for assigning coding agents (like Copilot) to issues. + * These functions use GitHub REST APIs. */ /** @@ -46,20 +42,47 @@ function getAgentName(assignee) { * @returns {Promise} */ async function getAvailableAgentLogins(owner, repo, githubClient = github) { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { - nodes { ... on Bot { login __typename } } + if (!githubClient?.rest?.issues?.checkUserCanBeAssigned && githubClient?.graphql) { + const query = ` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { + nodes { ... on Bot { login __typename } } + } } } + `; + try { + const response = await githubClient.graphql(query, { owner, repo }); + const actors = response.repository?.suggestedActors?.nodes || []; + const knownValues = Object.values(AGENT_LOGIN_NAMES); + const available = actors.filter(actor => actor?.login && knownValues.includes(actor.login)).map(actor => actor.login); + return available.sort(); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + core.debug(`Failed to list available agent logins: ${errorMessage}`); + return []; + } + } + + const knownValues = Object.values(AGENT_LOGIN_NAMES); + const available = []; + for (const login of knownValues) { + try { + await githubClient.rest.issues.checkUserCanBeAssigned({ + owner, + repo, + assignee: login, + }); + available.push(login); + } catch (e) { + const status = e && typeof e === "object" && "status" in e ? e.status : undefined; + if (status !== 404) { + core.debug(`Failed to check assignability for ${login}: ${getErrorMessage(e)}`); + } } - `; + } try { - const response = await githubClient.graphql(query, { owner, repo }); - const actors = response.repository?.suggestedActors?.nodes || []; - const knownValues = Object.values(AGENT_LOGIN_NAMES); - const available = actors.filter(actor => actor?.login && knownValues.includes(actor.login)).map(actor => actor.login); return available.sort(); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); @@ -69,7 +92,7 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) { } /** - * Find an agent in repository's suggested actors using GraphQL + * Find an agent that can be assigned in the repository using REST * @param {string} owner - Repository owner * @param {string} repo - Repository name * @param {string} agentName - Agent name (copilot) @@ -77,40 +100,60 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) { * @returns {Promise} Agent ID or null if not found */ async function findAgent(owner, repo, agentName, githubClient = github) { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { - nodes { - ... on Bot { - id - login - __typename + const loginName = AGENT_LOGIN_NAMES[agentName]; + if (!loginName) { + core.error(`Unknown agent: ${agentName}. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`); + return null; + } + + if (!githubClient?.rest?.issues?.checkUserCanBeAssigned && githubClient?.graphql) { + const query = ` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { + nodes { + ... on Bot { + id + login + __typename + } } } } } - } - `; - - try { - const response = await githubClient.graphql(query, { owner, repo }); - const actors = response.repository.suggestedActors.nodes; - - const loginName = AGENT_LOGIN_NAMES[agentName]; - if (!loginName) { - core.error(`Unknown agent: ${agentName}. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`); + `; + try { + const response = await githubClient.graphql(query, { owner, repo }); + const actors = response.repository.suggestedActors.nodes; + const agent = actors.find(actor => actor.login === loginName); + if (agent) return agent.id; + const knownValues = Object.values(AGENT_LOGIN_NAMES); + const available = actors.filter(a => a?.login && knownValues.includes(a.login)).map(a => a.login); + core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`); + if (available.length > 0) core.info(`Available assignable coding agents: ${available.join(", ")}`); + else core.info("No coding agents are currently assignable in this repository."); + if (agentName === "copilot") { + core.info("Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot"); + } return null; + } catch (error) { + const errorMessage = getErrorMessage(error); + core.error(`Failed to find ${agentName} agent: ${errorMessage}`); + throw error; } + } - const agent = actors.find(actor => actor.login === loginName); - if (agent) { - return agent.id; - } - - const knownValues = Object.values(AGENT_LOGIN_NAMES); - const available = actors.filter(a => a?.login && knownValues.includes(a.login)).map(a => a.login); - + try { + await githubClient.rest.issues.checkUserCanBeAssigned({ + owner, + repo, + assignee: loginName, + }); + return loginName; + } catch (error) { + const errorMessage = getErrorMessage(error); + core.error(`Failed to find ${agentName} agent: ${errorMessage}`); + const available = await getAvailableAgentLogins(owner, repo, githubClient); core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`); if (available.length > 0) { core.info(`Available assignable coding agents: ${available.join(", ")}`); @@ -120,146 +163,165 @@ async function findAgent(owner, repo, agentName, githubClient = github) { if (agentName === "copilot") { core.info("Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot"); } - return null; - } catch (error) { - const errorMessage = getErrorMessage(error); - core.error(`Failed to find ${agentName} agent: ${errorMessage}`); - - // Re-throw authentication/permission errors so they can be handled by the caller - // This allows if-missing: ignore logic to work properly - if ( - errorMessage.includes("Bad credentials") || - errorMessage.includes("Not Authenticated") || - errorMessage.includes("Resource not accessible") || - errorMessage.includes("Insufficient permissions") || - errorMessage.includes("requires authentication") - ) { - throw error; - } - return null; } } /** - * Get issue details (ID and current assignees) using GraphQL + * Get issue details (context and current assignees) using REST * @param {string} owner - Repository owner * @param {string} repo - Repository name * @param {number} issueNumber - Issue number * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) - * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>}|null>} + * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string}|null>} */ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) { - const query = ` - query($owner: String!, $repo: String!, $issueNumber: Int!) { - repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { - id - assignees(first: 100) { - nodes { - id - login + if (!githubClient?.rest?.issues?.get && githubClient?.graphql) { + const query = ` + query($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + id + assignees(first: 100) { + nodes { + id + login + } } } } } + `; + try { + const response = await githubClient.graphql(query, { owner, repo, issueNumber }); + const issue = response.repository.issue; + if (!issue || !issue.id) { + core.error("Could not get issue data"); + return null; + } + const currentAssignees = issue.assignees.nodes.map(assignee => ({ + id: assignee.id, + login: assignee.login, + })); + return { issueId: issue.id, currentAssignees, htmlUrl: "", title: "", body: "" }; + } catch (error) { + const errorMessage = getErrorMessage(error); + core.error(`Failed to get issue details: ${errorMessage}`); + throw error; } - `; + } try { - const response = await githubClient.graphql(query, { owner, repo, issueNumber }); - const issue = response.repository.issue; - - if (!issue || !issue.id) { + const { data: issue } = await githubClient.rest.issues.get({ owner, repo, issue_number: issueNumber }); + if (!issue || !issue.number) { core.error("Could not get issue data"); return null; } - - const currentAssignees = issue.assignees.nodes.map(assignee => ({ - id: assignee.id, + const currentAssignees = (issue.assignees || []).map(assignee => ({ + id: assignee.login, login: assignee.login, })); return { - issueId: issue.id, + issueId: `${owner}/${repo}#issue:${issue.number}`, currentAssignees, + htmlUrl: issue.html_url || "", + title: issue.title || "", + body: issue.body || "", }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get issue details: ${errorMessage}`); - // Re-throw the error to preserve the original error message for permission error detection throw error; } } /** - * Get pull request details (ID and current assignees) using GraphQL + * Get pull request details (context and current assignees) using REST * @param {string} owner - Repository owner * @param {string} repo - Repository name * @param {number} pullNumber - Pull request number * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) - * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>}|null>} + * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string}|null>} */ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = github) { - const query = ` - query($owner: String!, $repo: String!, $pullNumber: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $pullNumber) { - id - assignees(first: 100) { - nodes { - id - login + if (!githubClient?.rest?.pulls?.get && githubClient?.graphql) { + const query = ` + query($owner: String!, $repo: String!, $pullNumber: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pullNumber) { + id + assignees(first: 100) { + nodes { + id + login + } } } } } + `; + try { + const response = await githubClient.graphql(query, { owner, repo, pullNumber }); + const pullRequest = response.repository.pullRequest; + if (!pullRequest || !pullRequest.id) { + core.error("Could not get pull request data"); + return null; + } + const currentAssignees = pullRequest.assignees.nodes.map(assignee => ({ + id: assignee.id, + login: assignee.login, + })); + return { pullRequestId: pullRequest.id, currentAssignees, htmlUrl: "", title: "", body: "" }; + } catch (error) { + const errorMessage = getErrorMessage(error); + core.error(`Failed to get pull request details: ${errorMessage}`); + throw error; } - `; + } try { - const response = await githubClient.graphql(query, { owner, repo, pullNumber }); - const pullRequest = response.repository.pullRequest; - - if (!pullRequest || !pullRequest.id) { + const { data: pullRequest } = await githubClient.rest.pulls.get({ owner, repo, pull_number: pullNumber }); + if (!pullRequest || !pullRequest.number) { core.error("Could not get pull request data"); return null; } - - const currentAssignees = pullRequest.assignees.nodes.map(assignee => ({ - id: assignee.id, + const currentAssignees = (pullRequest.assignees || []).map(assignee => ({ + id: assignee.login, login: assignee.login, })); return { - pullRequestId: pullRequest.id, + pullRequestId: `${owner}/${repo}#pull:${pullRequest.number}`, currentAssignees, + htmlUrl: pullRequest.html_url || "", + title: pullRequest.title || "", + body: pullRequest.body || "", }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get pull request details: ${errorMessage}`); - // Re-throw the error to preserve the original error message for permission error detection throw error; } } /** - * Assign agent to issue or pull request using GraphQL replaceActorsForAssignable mutation - * @param {string} assignableId - GitHub issue or pull request ID - * @param {string} agentId - Agent ID + * Start an agent task for issue or pull request context using REST + * @param {string} assignableId - Synthetic target ID in format owner/repo#issue:N or owner/repo#pull:N + * @param {string} agentId - Agent login name * @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} pullRequestRepoId - Optional pull request repository ID for specifying where the PR should be created (GitHub agentAssignment.targetRepositoryId) + * @param {string|null} pullRequestRepoId - Optional pull request repository slug (owner/repo) where PR should be created * @param {string|null} model - Optional AI model to use (e.g., "claude-opus-4.6", "auto") * @param {string|null} customAgent - Optional custom agent ID for custom agents * @param {string|null} customInstructions - Optional custom instructions for the agent - * @param {string|null} baseBranch - Optional base branch for the PR (uses GraphQL baseRef field) + * @param {string|null} baseBranch - Optional base branch for the PR (REST base_ref field) * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) * @returns {Promise} True if successful */ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null, pullRequestRepoId = null, model = null, customAgent = null, customInstructions = null, baseBranch = null, githubClient = github) { - // SECURITY: pullRequestRepoId specifies a cross-repo target (targetRepositoryId). + // SECURITY: pullRequestRepoId specifies a cross-repo target repository slug. // Callers MUST validate the corresponding repository slug against allowedRepos using // validateTargetRepo (from repo_helpers.cjs) before invoking this function. // Filter current assignees based on allowed list (if configured) @@ -281,130 +343,133 @@ 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 agentAssignment object if any agent-specific parameters are provided - const hasAgentAssignment = pullRequestRepoId || model || customAgent || customInstructions || baseBranch; - - // Build the mutation - conditionally include agentAssignment if any parameters are provided - let mutation; - let variables; - - if (hasAgentAssignment) { - // Build agentAssignment object with only the fields that are provided - const agentAssignmentFields = []; - const agentAssignmentParams = []; - - if (pullRequestRepoId) { - agentAssignmentFields.push("targetRepositoryId: $targetRepoId"); - agentAssignmentParams.push("$targetRepoId: ID!"); - } - if (model) { - agentAssignmentFields.push("model: $model"); - agentAssignmentParams.push("$model: String!"); - } - if (customAgent) { - agentAssignmentFields.push("customAgent: $customAgent"); - agentAssignmentParams.push("$customAgent: String!"); - } - if (customInstructions) { - agentAssignmentFields.push("customInstructions: $customInstructions"); - agentAssignmentParams.push("$customInstructions: String!"); - } - if (baseBranch) { - agentAssignmentFields.push("baseRef: $baseRef"); - agentAssignmentParams.push("$baseRef: String!"); - } - - // Build the mutation with agentAssignment - const allParams = ["$assignableId: ID!", "$actorIds: [ID!]!", ...agentAssignmentParams].join(", "); - const assignmentFields = agentAssignmentFields.join("\n "); - - mutation = ` - mutation(${allParams}) { - replaceActorsForAssignable(input: { - assignableId: $assignableId, - actorIds: $actorIds, - agentAssignment: { - ${assignmentFields} + if (!githubClient?.request && githubClient?.graphql) { + const actorIds = [agentId, ...filteredAssignees.map(a => a.id).filter(id => id !== agentId)]; + const hasAgentAssignment = pullRequestRepoId || model || customAgent || customInstructions || baseBranch; + let mutation; + let variables; + if (hasAgentAssignment) { + const agentAssignmentFields = []; + const agentAssignmentParams = []; + if (pullRequestRepoId) { + agentAssignmentFields.push("targetRepositoryId: $targetRepoId"); + agentAssignmentParams.push("$targetRepoId: ID!"); + } + if (model) { + agentAssignmentFields.push("model: $model"); + agentAssignmentParams.push("$model: String!"); + } + if (customAgent) { + agentAssignmentFields.push("customAgent: $customAgent"); + agentAssignmentParams.push("$customAgent: String!"); + } + if (customInstructions) { + agentAssignmentFields.push("customInstructions: $customInstructions"); + agentAssignmentParams.push("$customInstructions: String!"); + } + if (baseBranch) { + agentAssignmentFields.push("baseRef: $baseRef"); + agentAssignmentParams.push("$baseRef: String!"); + } + const allParams = ["$assignableId: ID!", "$actorIds: [ID!]!", ...agentAssignmentParams].join(", "); + const assignmentFields = agentAssignmentFields.join("\n "); + mutation = ` + mutation(${allParams}) { + replaceActorsForAssignable(input: { + assignableId: $assignableId, + actorIds: $actorIds, + agentAssignment: { + ${assignmentFields} + } + }) { + __typename } - }) { - __typename } - } - `; - - variables = { - assignableId, - actorIds, - ...(pullRequestRepoId && { targetRepoId: pullRequestRepoId }), - ...(model && { model }), - ...(customAgent && { customAgent }), - ...(customInstructions && { customInstructions }), - ...(baseBranch && { baseRef: baseBranch }), - }; - } else { - // Standard mutation without agentAssignment - mutation = ` - mutation($assignableId: ID!, $actorIds: [ID!]!) { - replaceActorsForAssignable(input: { - assignableId: $assignableId, - actorIds: $actorIds - }) { - __typename + `; + variables = { + assignableId, + actorIds, + ...(pullRequestRepoId && { targetRepoId: pullRequestRepoId }), + ...(model && { model }), + ...(customAgent && { customAgent }), + ...(customInstructions && { customInstructions }), + ...(baseBranch && { baseRef: baseBranch }), + }; + } else { + mutation = ` + mutation($assignableId: ID!, $actorIds: [ID!]!) { + replaceActorsForAssignable(input: { + assignableId: $assignableId, + actorIds: $actorIds + }) { + __typename + } } - } - `; - variables = { - assignableId, - actorIds, - }; + `; + variables = { assignableId, actorIds }; + } + try { + const response = await githubClient.graphql(mutation, { + ...variables, + headers: { + "GraphQL-Features": "issues_copilot_assignment_api_support,coding_agent_model_selection", + }, + }); + return !!response?.replaceActorsForAssignable?.__typename; + } catch (error) { + core.error(`Failed to assign ${agentName}: ${getErrorMessage(error)}`); + return false; + } + } + + const targetMatch = /^(?[^/]+)\/(?[^#]+)#(?issue|pull):(?\d+)$/.exec(assignableId); + if (!targetMatch || !targetMatch.groups) { + core.error(`Invalid assignment context: ${assignableId}`); + return false; + } + const sourceOwner = targetMatch.groups.sourceOwner; + const sourceRepo = targetMatch.groups.sourceRepo; + const itemType = targetMatch.groups.itemType === "pull" ? "pull request" : "issue"; + const itemNumber = targetMatch.groups.itemNumber; + const sourceUrl = `https://github.com/${sourceOwner}/${sourceRepo}/${itemType === "pull request" ? "pull" : "issues"}/${itemNumber}`; + const targetRepoSlug = pullRequestRepoId || `${sourceOwner}/${sourceRepo}`; + const targetParts = targetRepoSlug.split("/"); + if (targetParts.length !== 2) { + core.error(`Invalid target repository slug: ${targetRepoSlug}`); + return false; } + const targetOwner = targetParts[0]; + const targetRepo = targetParts[1]; + const promptParts = [`Start work for ${itemType} ${sourceOwner}/${sourceRepo}#${itemNumber}.`, `Use this as the primary context: ${sourceUrl}`]; + if (targetRepoSlug !== `${sourceOwner}/${sourceRepo}`) promptParts.push(`Create the branch and pull request in ${targetRepoSlug}.`); + if (customAgent) promptParts.push(`Custom agent: ${customAgent}`); + if (customInstructions) promptParts.push(`Additional instructions:\n${customInstructions}`); + const prompt = promptParts.join("\n\n"); try { - core.info("Executing agent assignment GraphQL mutation"); - - // Build debug log message with all parameters - const debugParts = [ - `assignableId=${assignableId}`, - `actorIds=${JSON.stringify(actorIds)}`, - ...(pullRequestRepoId ? [`targetRepoId=${pullRequestRepoId}`] : []), - ...(model ? [`model=${model}`] : []), - ...(customAgent ? [`customAgent=${customAgent}`] : []), - ...(customInstructions ? [`customInstructions=${customInstructions.substring(0, 50)}...`] : []), - ...(baseBranch ? [`baseRef=${baseBranch}`] : []), - ]; - core.debug(`GraphQL mutation with variables: ${debugParts.join(", ")}`); - - // Both feature flags are required per GitHub Copilot documentation - const graphqlFeatures = "issues_copilot_assignment_api_support,coding_agent_model_selection"; - - const response = await githubClient.graphql(mutation, { - ...variables, - headers: { - "GraphQL-Features": graphqlFeatures, - }, + core.info("Starting agent task via REST API"); + const response = await githubClient.request("POST /agents/repos/{owner}/{repo}/tasks", { + owner: targetOwner, + repo: targetRepo, + prompt, + create_pull_request: true, + ...(model ? { model } : {}), + ...(baseBranch ? { base_ref: baseBranch } : {}), + headers: { "X-GitHub-Api-Version": "2026-03-10" }, }); - - if (response?.replaceActorsForAssignable?.__typename) { - return true; - } + if (response?.data?.id) return true; core.error("Unexpected response from GitHub API"); return false; } catch (error) { const errorMessage = getErrorMessage(error); - // Check for 502 Bad Gateway errors - these often occur but the assignment still succeeds - // prettier-ignore - const err = /** @type {any} */ (error); + const err = /** @type {any} */ error; const is502Error = err?.response?.status === 502 || errorMessage.includes("502 Bad Gateway"); if (is502Error) { - core.warning(`Received 502 error from cloud gateway during agent assignment, but assignment may have succeeded`); + core.warning(`Received 502 error from cloud gateway during agent task creation, but task may have been created`); core.info(`502 error details logged for troubleshooting`); - // Log the 502 error details without failing try { if (error && typeof error === "object") { const details = { @@ -426,30 +491,26 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent core.debug(`Failed to serialize 502 error details: ${loggingErrMsg}`); } - // Treat 502 as success since assignment typically succeeds despite the error - core.info(`Treating 502 error as success - agent assignment likely completed`); + core.info(`Treating 502 error as success - agent task likely created`); return true; } - // Debug: surface the raw GraphQL error structure for troubleshooting fine-grained permission issues + // Debug: surface the raw REST error structure for troubleshooting fine-grained permission issues try { - core.debug(`Raw GraphQL error message: ${errorMessage}`); + core.debug(`Raw REST error message: ${errorMessage}`); if (error && typeof error === "object") { - // Common GraphQL error shapes: error.errors (array), error.data, error.response const details = { ...(err.errors && { errors: err.errors }), ...(err.response && { response: err.response }), ...(err.data && { data: err.data }), }; - // If GitHub returns an array of errors with 'type'/'message' if (Array.isArray(err.errors)) { details.compactMessages = err.errors.map(e => e.message).filter(Boolean); } const serialized = JSON.stringify(details, null, 2); if (serialized !== "{}") { - core.debug(`Raw GraphQL error details: ${serialized}`); - // Also emit non-debug version so users without ACTIONS_STEP_DEBUG can see it - core.error("Raw GraphQL error details (for troubleshooting):"); + core.debug(`Raw REST error details: ${serialized}`); + core.error("Raw REST error details (for troubleshooting):"); serialized .split("\n") .filter(line => line.trim()) @@ -457,90 +518,16 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent } } } catch (loggingErr) { - // Never fail assignment because of debug logging const loggingErrMsg = loggingErr instanceof Error ? loggingErr.message : String(loggingErr); - core.debug(`Failed to serialize GraphQL error details: ${loggingErrMsg}`); + core.debug(`Failed to serialize REST error details: ${loggingErrMsg}`); } - // Check for permission-related errors - if (errorMessage.includes("Resource not accessible by personal access token") || errorMessage.includes("Resource not accessible by integration") || errorMessage.includes("Insufficient permissions to assign")) { - // Attempt fallback mutation addAssigneesToAssignable when replaceActorsForAssignable is forbidden - core.info("Primary mutation replaceActorsForAssignable forbidden. Attempting fallback addAssigneesToAssignable..."); - try { - // Build agentAssignment for fallback mutation (same parameters as primary) - const fallbackAgentAssignmentFields = []; - const fallbackAgentAssignmentParams = []; - const fallbackVariables = { assignableId, assigneeIds: [agentId] }; - - if (pullRequestRepoId) { - fallbackAgentAssignmentFields.push("targetRepositoryId: $targetRepoId"); - fallbackAgentAssignmentParams.push("$targetRepoId: ID!"); - fallbackVariables.targetRepoId = pullRequestRepoId; - } - if (model) { - fallbackAgentAssignmentFields.push("model: $model"); - fallbackAgentAssignmentParams.push("$model: String!"); - fallbackVariables.model = model; - } - if (customAgent) { - fallbackAgentAssignmentFields.push("customAgent: $customAgent"); - fallbackAgentAssignmentParams.push("$customAgent: String!"); - fallbackVariables.customAgent = customAgent; - } - if (customInstructions) { - fallbackAgentAssignmentFields.push("customInstructions: $customInstructions"); - fallbackAgentAssignmentParams.push("$customInstructions: String!"); - fallbackVariables.customInstructions = customInstructions; - } - if (baseBranch) { - fallbackAgentAssignmentFields.push("baseRef: $baseRef"); - fallbackAgentAssignmentParams.push("$baseRef: String!"); - fallbackVariables.baseRef = baseBranch; - } - - const hasFallbackAgentAssignment = fallbackAgentAssignmentFields.length > 0; - const fallbackBaseParams = ["$assignableId: ID!", "$assigneeIds: [ID!]!", ...fallbackAgentAssignmentParams].join(", "); - const fallbackMutation = hasFallbackAgentAssignment - ? ` - mutation(${fallbackBaseParams}) { - addAssigneesToAssignable(input: { - assignableId: $assignableId, - assigneeIds: $assigneeIds, - agentAssignment: { - ${fallbackAgentAssignmentFields.join("\n ")} - } - }) { - clientMutationId - } - } - ` - : ` - mutation($assignableId: ID!, $assigneeIds: [ID!]!) { - addAssigneesToAssignable(input: { - assignableId: $assignableId, - assigneeIds: $assigneeIds - }) { - clientMutationId - } - } - `; - core.info("Executing fallback agent assignment GraphQL mutation"); - core.debug(`Fallback GraphQL mutation with variables: assignableId=${assignableId}, assigneeIds=[${agentId}]`); - const fallbackResp = await githubClient.graphql(fallbackMutation, { - ...fallbackVariables, - headers: { - "GraphQL-Features": "issues_copilot_assignment_api_support,coding_agent_model_selection", - }, - }); - if (fallbackResp?.addAssigneesToAssignable) { - core.info(`Fallback succeeded: agent '${agentName}' added via addAssigneesToAssignable.`); - return true; - } - core.warning("Fallback mutation returned unexpected response; proceeding with permission guidance."); - } catch (fallbackError) { - const fallbackErrMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); - core.error(`Fallback addAssigneesToAssignable failed: ${fallbackErrMsg}`); - } + if ( + errorMessage.includes("Resource not accessible by personal access token") || + errorMessage.includes("Resource not accessible by integration") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("requires authentication") + ) { logPermissionError(agentName); } else { core.error(`Failed to assign ${agentName}: ${errorMessage}`); @@ -557,25 +544,24 @@ function logPermissionError(agentName) { core.error(`Failed to assign ${agentName}: Insufficient permissions`); core.error(""); core.error("Assigning Copilot coding agent requires:"); - core.error(" 1. All four workflow permissions:"); + core.error(" 1. Repository permissions:"); core.error(" - actions: write"); core.error(" - contents: write"); - core.error(" - issues: write"); - core.error(" - pull-requests: write"); + core.error(" - Agent tasks: read and write"); core.error(""); - core.error(" 2. A classic PAT with 'repo' scope OR fine-grained PAT with explicit Write permissions above:"); - core.error(" (Fine-grained PATs must grant repository access + write for Issues, Pull requests, Contents, Actions)"); + core.error(" 2. A fine-grained PAT or GitHub App user token with Agent tasks (read/write)"); + core.error(" (Installation tokens are not supported for agent task creation)"); core.error(""); core.error(" 3. Repository settings:"); core.error(" - Actions must have write permissions"); core.error(" - Go to: Settings > Actions > General > Workflow permissions"); core.error(" - Select: 'Read and write permissions'"); core.error(""); - core.error(" 4. Organization/Enterprise settings:"); + core.error(" 4. Organization/Enterprise settings and Copilot policy:"); core.error(" - Check if your org restricts bot assignments"); core.error(" - Verify Copilot is enabled for your repository"); core.error(""); - core.info("For more information, see: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr"); + core.info("For more information, see: https://docs.github.com/en/rest/agent-tasks/agent-tasks?apiVersion=2026-03-10#start-a-task"); } /** @@ -592,28 +578,27 @@ Assigning Copilot coding agent requires **ALL** of these permissions: permissions: actions: write contents: write - issues: write - pull-requests: write + agent-tasks: write \`\`\` **Token capability note:** -- Current token (PAT or GITHUB_TOKEN) lacks assignee mutation capability for this repository. -- Both \`replaceActorsForAssignable\` and fallback \`addAssigneesToAssignable\` returned FORBIDDEN/Resource not accessible. -- This typically means bot/user assignment requires an elevated OAuth or GitHub App installation token. +- Current token lacks permission for \`POST /agents/repos/{owner}/{repo}/tasks\`. +- Agent task creation requires a fine-grained PAT or GitHub App user token with **Agent tasks: read and write**. +- GitHub App installation access tokens are not supported for this endpoint. **Recommended remediation paths:** -1. Create & install a GitHub App with: Issues/Pull requests/Contents/Actions (write) → use installation token in job. -2. Manual assignment: add the agent through the UI until broader token support is available. -3. Open a support ticket referencing failing mutation \`replaceActorsForAssignable\` and repository slug. +1. Use a fine-grained PAT with repository access and **Agent tasks (read/write)**. +2. Use a GitHub App **user access token** (not installation token) with Agent tasks permission. +3. Verify Copilot coding agent is enabled for the repository and organization policy allows task creation. -**Why this failed:** Fine-grained and classic PATs can update issue title (verified) but not modify assignees in this environment. +**Why this failed:** The token could not create an agent task via the REST API. -📖 Reference: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr (general agent docs) +📖 Reference: https://docs.github.com/en/rest/agent-tasks/agent-tasks?apiVersion=2026-03-10#start-a-task `; } /** - * Assign an agent to an issue using GraphQL + * Assign an agent to an issue by starting an agent task using REST * This is the main entry point for assigning agents from other scripts * @param {string} owner - Repository owner * @param {string} repo - Repository name @@ -642,14 +627,14 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) { } core.info(`Found ${agentName} coding agent (ID: ${agentId})`); - // Get issue details (ID and current assignees) via GraphQL + // Get issue details and current assignees via REST core.info("Getting issue details..."); const issueDetails = await getIssueDetails(owner, repo, issueNumber); if (!issueDetails) { return { success: false, error: "Failed to get issue details" }; } - core.info(`Issue ID: ${issueDetails.issueId}`); + core.info(`Issue context: ${issueDetails.issueId}`); // Check if agent is already assigned if (issueDetails.currentAssignees.some(a => a.id === agentId)) { @@ -657,12 +642,12 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) { return { success: true }; } - // Assign agent using GraphQL mutation (no allowed list filtering in this helper) + // Assign agent by starting a REST task (no allowed list filtering in this helper) core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null); if (!success) { - return { success: false, error: `Failed to assign ${agentName} via GraphQL` }; + return { success: false, error: `Failed to assign ${agentName} via REST` }; } core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 78e5b2a0be2..2bdb21f6db8 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -258,18 +258,13 @@ async function main(config = {}) { return { success: false, error }; } try { - const itemPullRequestRepoQuery = ` - query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { id } - } - `; - const itemPullRequestRepoResponse = await githubClient.graphql(itemPullRequestRepoQuery, { owner: pullRequestRepoParts[0], name: pullRequestRepoParts[1] }); - effectivePullRequestRepoId = itemPullRequestRepoResponse.repository.id; + await resolvePullRequestRepo(githubClient, pullRequestRepoParts[0], pullRequestRepoParts[1], configuredBaseBranch); + effectivePullRequestRepoId = itemPullRequestRepo; effectivePullRequestRepoSlug = itemPullRequestRepo; hasValidatedPerItemPullRequestRepoOverride = true; - core.info(`Using per-item pull request repository: ${itemPullRequestRepo} (ID: ${effectivePullRequestRepoId})`); + core.info(`Using per-item pull request repository: ${itemPullRequestRepo}`); } catch (error) { - const errorMsg = `Failed to fetch pull request repository ID for ${itemPullRequestRepo}: ${getErrorMessage(error)}`; + const errorMsg = `Failed to resolve pull request repository for ${itemPullRequestRepo}: ${getErrorMessage(error)}`; core.error(errorMsg); _allResults.push({ issue_number: message.issue_number || null, pull_number: message.pull_number || null, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, success: false, error: errorMsg }); return { success: false, error: errorMsg }; @@ -391,7 +386,7 @@ async function main(config = {}) { if (effectiveBaseBranch) core.info(`Using base branch: ${effectiveBaseBranch}`); const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, effectivePullRequestRepoId, model, customAgent, customInstructions, effectiveBaseBranch, githubClient); - if (!success) throw new Error(`Failed to assign ${agentName} via GraphQL`); + if (!success) throw new Error(`Failed to assign ${agentName} via REST`); core.info(`Successfully assigned ${agentName} coding agent to ${type} #${number}`); _allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true }); diff --git a/actions/setup/js/pr_helpers.cjs b/actions/setup/js/pr_helpers.cjs index 414c0bf92cf..1279ef89d34 100644 --- a/actions/setup/js/pr_helpers.cjs +++ b/actions/setup/js/pr_helpers.cjs @@ -71,8 +71,8 @@ function getPullRequestNumber(messageItem, context) { } /** - * Resolves the pull request repository ID and effective base branch. - * Fetches `id` and `defaultBranchRef.name` from the GitHub API. + * Resolves pull request repository context and effective base branch. + * Fetches repository metadata from the GitHub REST API. * The effective base branch is the explicitly configured branch (if any), * falling back to the repository's actual default branch. * @@ -83,17 +83,25 @@ function getPullRequestNumber(messageItem, context) { * @returns {Promise<{repoId: string, effectiveBaseBranch: string|null, resolvedDefaultBranch: string|null}>} */ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) { - const query = ` - query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - id - defaultBranchRef { name } + if (!github?.rest?.repos?.get && github?.graphql) { + const query = ` + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + id + defaultBranchRef { name } + } } - } - `; - const response = await github.graphql(query, { owner, name: repo }); - const repoId = response.repository.id; - const resolvedDefaultBranch = response.repository.defaultBranchRef?.name ?? null; + `; + const response = await github.graphql(query, { owner, name: repo }); + const repoId = response.repository.id; + const resolvedDefaultBranch = response.repository.defaultBranchRef?.name ?? null; + const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; + return { repoId, effectiveBaseBranch, resolvedDefaultBranch }; + } + + const { data } = await github.rest.repos.get({ owner, repo }); + const repoId = `${owner}/${repo}`; + const resolvedDefaultBranch = data.default_branch ?? null; const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; return { repoId, effectiveBaseBranch, resolvedDefaultBranch }; } From 3a538be172e1f9112ff6ca72036da9f8aec19daa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:14:01 +0000 Subject: [PATCH 2/9] fix: complete REST migration for assign-to-agent helpers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 81 ++++++++++++++--------- actions/setup/js/assign_to_agent.cjs | 23 ++++++- actions/setup/js/pr_helpers.cjs | 8 +-- 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index d979ad1e097..255fb87fd48 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -82,13 +82,7 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) { } } } - try { - return available.sort(); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - core.debug(`Failed to list available agent logins: ${errorMessage}`); - return []; - } + return available.sort(); } /** @@ -139,7 +133,16 @@ async function findAgent(owner, repo, agentName, githubClient = github) { } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to find ${agentName} agent: ${errorMessage}`); - throw error; + if ( + errorMessage.includes("Bad credentials") || + errorMessage.includes("Not Authenticated") || + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("requires authentication") + ) { + throw error; + } + return null; } } @@ -149,7 +152,8 @@ async function findAgent(owner, repo, agentName, githubClient = github) { repo, assignee: loginName, }); - return loginName; + const { data: agentUser } = await githubClient.rest.users.getByUsername({ username: loginName }); + return String(agentUser.id); } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to find ${agentName} agent: ${errorMessage}`); @@ -173,7 +177,7 @@ async function findAgent(owner, repo, agentName, githubClient = github) { * @param {string} repo - Repository name * @param {number} issueNumber - Issue number * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) - * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string}|null>} + * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "issue", number: number}}|null>} */ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) { if (!githubClient?.rest?.issues?.get && githubClient?.graphql) { @@ -203,7 +207,7 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) id: assignee.id, login: assignee.login, })); - return { issueId: issue.id, currentAssignees, htmlUrl: "", title: "", body: "" }; + return { issueId: issue.id, currentAssignees, htmlUrl: "", title: "", body: "", taskContext: { owner, repo, type: "issue", number: issueNumber } }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get issue details: ${errorMessage}`); @@ -218,16 +222,17 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) return null; } const currentAssignees = (issue.assignees || []).map(assignee => ({ - id: assignee.login, + id: String(assignee.id), login: assignee.login, })); return { - issueId: `${owner}/${repo}#issue:${issue.number}`, + issueId: String(issue.id), currentAssignees, htmlUrl: issue.html_url || "", title: issue.title || "", body: issue.body || "", + taskContext: { owner, repo, type: "issue", number: issue.number }, }; } catch (error) { const errorMessage = getErrorMessage(error); @@ -242,7 +247,7 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) * @param {string} repo - Repository name * @param {number} pullNumber - Pull request number * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) - * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string}|null>} + * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "pull", number: number}}|null>} */ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = github) { if (!githubClient?.rest?.pulls?.get && githubClient?.graphql) { @@ -272,7 +277,7 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git id: assignee.id, login: assignee.login, })); - return { pullRequestId: pullRequest.id, currentAssignees, htmlUrl: "", title: "", body: "" }; + return { pullRequestId: pullRequest.id, currentAssignees, htmlUrl: "", title: "", body: "", taskContext: { owner, repo, type: "pull", number: pullNumber } }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get pull request details: ${errorMessage}`); @@ -287,16 +292,17 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git return null; } const currentAssignees = (pullRequest.assignees || []).map(assignee => ({ - id: assignee.login, + id: String(assignee.id), login: assignee.login, })); return { - pullRequestId: `${owner}/${repo}#pull:${pullRequest.number}`, + pullRequestId: String(pullRequest.id), currentAssignees, htmlUrl: pullRequest.html_url || "", title: pullRequest.title || "", body: pullRequest.body || "", + taskContext: { owner, repo, type: "pull", number: pullRequest.number }, }; } catch (error) { const errorMessage = getErrorMessage(error); @@ -312,15 +318,31 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git * @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} pullRequestRepoId - Optional pull request repository slug (owner/repo) where PR should be created + * @param {string|null} pullRequestRepoId - Optional pull request repository ID for GraphQL fallback * @param {string|null} model - Optional AI model to use (e.g., "claude-opus-4.6", "auto") * @param {string|null} customAgent - Optional custom agent ID for custom agents * @param {string|null} customInstructions - Optional custom instructions for the agent * @param {string|null} baseBranch - Optional base branch for the PR (REST base_ref field) * @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github) + * @param {{owner: string, repo: string, type: "issue"|"pull", number: number}|null} [taskContext] - Source issue/PR context for REST path + * @param {string|null} [pullRequestRepoSlug] - Optional pull request repository slug (owner/repo) for REST path * @returns {Promise} True if successful */ -async function assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents = null, pullRequestRepoId = null, model = null, customAgent = null, customInstructions = null, baseBranch = null, githubClient = github) { +async function assignAgentToIssue( + assignableId, + agentId, + currentAssignees, + agentName, + allowedAgents = null, + pullRequestRepoId = null, + model = null, + customAgent = null, + customInstructions = null, + baseBranch = null, + githubClient = github, + taskContext = null, + pullRequestRepoSlug = null +) { // SECURITY: pullRequestRepoId specifies a cross-repo target repository slug. // Callers MUST validate the corresponding repository slug against allowedRepos using // validateTargetRepo (from repo_helpers.cjs) before invoking this function. @@ -422,17 +444,16 @@ async function assignAgentToIssue(assignableId, agentId, currentAssignees, agent } } - const targetMatch = /^(?[^/]+)\/(?[^#]+)#(?issue|pull):(?\d+)$/.exec(assignableId); - if (!targetMatch || !targetMatch.groups) { + if (!taskContext) { core.error(`Invalid assignment context: ${assignableId}`); return false; } - const sourceOwner = targetMatch.groups.sourceOwner; - const sourceRepo = targetMatch.groups.sourceRepo; - const itemType = targetMatch.groups.itemType === "pull" ? "pull request" : "issue"; - const itemNumber = targetMatch.groups.itemNumber; + const sourceOwner = taskContext.owner; + const sourceRepo = taskContext.repo; + const itemType = taskContext.type === "pull" ? "pull request" : "issue"; + const itemNumber = String(taskContext.number); const sourceUrl = `https://github.com/${sourceOwner}/${sourceRepo}/${itemType === "pull request" ? "pull" : "issues"}/${itemNumber}`; - const targetRepoSlug = pullRequestRepoId || `${sourceOwner}/${sourceRepo}`; + const targetRepoSlug = pullRequestRepoSlug || `${sourceOwner}/${sourceRepo}`; const targetParts = targetRepoSlug.split("/"); if (targetParts.length !== 2) { core.error(`Invalid target repository slug: ${targetRepoSlug}`); @@ -547,9 +568,9 @@ function logPermissionError(agentName) { core.error(" 1. Repository permissions:"); core.error(" - actions: write"); core.error(" - contents: write"); - core.error(" - Agent tasks: read and write"); + core.error(" - agent-tasks: write"); core.error(""); - core.error(" 2. A fine-grained PAT or GitHub App user token with Agent tasks (read/write)"); + core.error(" 2. A fine-grained PAT or GitHub App user token with agent-tasks: write"); core.error(" (Installation tokens are not supported for agent task creation)"); core.error(""); core.error(" 3. Repository settings:"); @@ -637,14 +658,14 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) { core.info(`Issue context: ${issueDetails.issueId}`); // Check if agent is already assigned - if (issueDetails.currentAssignees.some(a => a.id === agentId)) { + if (issueDetails.currentAssignees.some(a => a.id === agentId || a.login === AGENT_LOGIN_NAMES[agentName])) { core.info(`${agentName} is already assigned to issue #${issueNumber}`); return { success: true }; } // Assign agent by starting a REST task (no allowed list filtering in this helper) core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null, null, null, null, null, null, github, issueDetails.taskContext); if (!success) { return { success: false, error: `Failed to assign ${agentName} via REST` }; diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 2bdb21f6db8..63fe066a854 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -258,8 +258,8 @@ async function main(config = {}) { return { success: false, error }; } try { - await resolvePullRequestRepo(githubClient, pullRequestRepoParts[0], pullRequestRepoParts[1], configuredBaseBranch); - effectivePullRequestRepoId = itemPullRequestRepo; + const resolved = await resolvePullRequestRepo(githubClient, pullRequestRepoParts[0], pullRequestRepoParts[1], configuredBaseBranch); + effectivePullRequestRepoId = resolved.repoId; effectivePullRequestRepoSlug = itemPullRequestRepo; hasValidatedPerItemPullRequestRepoOverride = true; core.info(`Using per-item pull request repository: ${itemPullRequestRepo}`); @@ -347,16 +347,19 @@ async function main(config = {}) { core.info(`Getting ${type} details...`); let assignableId; let currentAssignees; + let taskContext = null; if (issueNumber) { const issueDetails = await getIssueDetails(effectiveOwner, effectiveRepo, issueNumber, githubClient); if (!issueDetails) throw new Error(`Failed to get issue details`); assignableId = issueDetails.issueId; currentAssignees = issueDetails.currentAssignees; + taskContext = issueDetails.taskContext || { owner: effectiveOwner, repo: effectiveRepo, type: "issue", number: issueNumber }; } else if (pullNumber) { const prDetails = await getPullRequestDetails(effectiveOwner, effectiveRepo, pullNumber, githubClient); if (!prDetails) throw new Error(`Failed to get pull request details`); assignableId = prDetails.pullRequestId; currentAssignees = prDetails.currentAssignees; + taskContext = prDetails.taskContext || { owner: effectiveOwner, repo: effectiveRepo, type: "pull", number: pullNumber }; } else { throw new Error(`No issue or pull request number available`); } @@ -385,7 +388,21 @@ async function main(config = {}) { if (customInstructions) core.info(`Using custom instructions: ${customInstructions.substring(0, 100)}${customInstructions.length > 100 ? "..." : ""}`); if (effectiveBaseBranch) core.info(`Using base branch: ${effectiveBaseBranch}`); - const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, effectivePullRequestRepoId, model, customAgent, customInstructions, effectiveBaseBranch, githubClient); + const success = await assignAgentToIssue( + assignableId, + agentId, + currentAssignees, + agentName, + allowedAgents, + effectivePullRequestRepoId, + model, + customAgent, + customInstructions, + effectiveBaseBranch, + githubClient, + taskContext, + effectivePullRequestRepoSlug + ); if (!success) throw new Error(`Failed to assign ${agentName} via REST`); core.info(`Successfully assigned ${agentName} coding agent to ${type} #${number}`); diff --git a/actions/setup/js/pr_helpers.cjs b/actions/setup/js/pr_helpers.cjs index 1279ef89d34..0cc3e8617a2 100644 --- a/actions/setup/js/pr_helpers.cjs +++ b/actions/setup/js/pr_helpers.cjs @@ -80,7 +80,7 @@ function getPullRequestNumber(messageItem, context) { * @param {string} owner * @param {string} repo * @param {string|null|undefined} configuredBaseBranch - explicitly configured base branch (may be null or undefined) - * @returns {Promise<{repoId: string, effectiveBaseBranch: string|null, resolvedDefaultBranch: string|null}>} + * @returns {Promise<{repoId: string, repoSlug: string, effectiveBaseBranch: string|null, resolvedDefaultBranch: string|null}>} */ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) { if (!github?.rest?.repos?.get && github?.graphql) { @@ -96,14 +96,14 @@ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) const repoId = response.repository.id; const resolvedDefaultBranch = response.repository.defaultBranchRef?.name ?? null; const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; - return { repoId, effectiveBaseBranch, resolvedDefaultBranch }; + return { repoId, repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; } const { data } = await github.rest.repos.get({ owner, repo }); - const repoId = `${owner}/${repo}`; + const repoId = data.node_id || `${owner}/${repo}`; const resolvedDefaultBranch = data.default_branch ?? null; const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; - return { repoId, effectiveBaseBranch, resolvedDefaultBranch }; + return { repoId, repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; } /** From 80d19b0e1afe2fc68a907b9c560d7dd155414c79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:37:49 +0000 Subject: [PATCH 3/9] fix: resolve test failures in assign_agent_helpers and assign_to_agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove stale graphql mock from findAgent 'unknown agent' test that leaked into subsequent tests (findAgent now checks loginName before calling graphql) - Strip extra fields (htmlUrl, title, body, taskContext) from GraphQL-path returns in getIssueDetails and getPullRequestDetails (those fields are not available from GraphQL) - Add 502 handling to the GraphQL fallback catch block in assignAgentToIssue, mirroring the REST path behaviour - Update generatePermissionErrorSummary test expectations from the old GraphQL-era strings (issues:write, replaceActorsForAssignable) to the current REST API strings (agent-tasks:write, POST /agents/repos/…/tasks) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 14 +++++++++++--- actions/setup/js/assign_agent_helpers.test.cjs | 14 ++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 255fb87fd48..58415b7cf82 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -207,7 +207,7 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) id: assignee.id, login: assignee.login, })); - return { issueId: issue.id, currentAssignees, htmlUrl: "", title: "", body: "", taskContext: { owner, repo, type: "issue", number: issueNumber } }; + return { issueId: issue.id, currentAssignees }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get issue details: ${errorMessage}`); @@ -277,7 +277,7 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git id: assignee.id, login: assignee.login, })); - return { pullRequestId: pullRequest.id, currentAssignees, htmlUrl: "", title: "", body: "", taskContext: { owner, repo, type: "pull", number: pullNumber } }; + return { pullRequestId: pullRequest.id, currentAssignees }; } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to get pull request details: ${errorMessage}`); @@ -439,7 +439,15 @@ async function assignAgentToIssue( }); return !!response?.replaceActorsForAssignable?.__typename; } catch (error) { - core.error(`Failed to assign ${agentName}: ${getErrorMessage(error)}`); + const errorMessage = getErrorMessage(error); + const err = /** @type {any} */ (error); + const is502Error = err?.response?.status === 502 || errorMessage.includes("502 Bad Gateway"); + if (is502Error) { + core.warning(`Received 502 error from cloud gateway during agent task creation, but task may have been created`); + core.info(`Treating 502 error as success - agent task likely created`); + return true; + } + core.error(`Failed to assign ${agentName}: ${errorMessage}`); return false; } } diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index 465a0506aa0..87bd31aea6f 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -132,15 +132,6 @@ describe("assign_agent_helpers.cjs", () => { }); it("should return null for unknown agent name", async () => { - // Need to mock GraphQL because the function calls it before checking agent name - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [], - }, - }, - }); - const result = await findAgent("owner", "repo", "unknown-agent"); expect(result).toBeNull(); @@ -500,9 +491,8 @@ describe("assign_agent_helpers.cjs", () => { expect(summary).toContain("### ⚠️ Permission Requirements"); expect(summary).toContain("actions: write"); expect(summary).toContain("contents: write"); - expect(summary).toContain("issues: write"); - expect(summary).toContain("pull-requests: write"); - expect(summary).toContain("replaceActorsForAssignable"); + expect(summary).toContain("agent-tasks: write"); + expect(summary).toContain("POST /agents/repos/{owner}/{repo}/tasks"); }); }); From eacea262d5a8b5849020c41268040f9907b715dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:28:37 +0000 Subject: [PATCH 4/9] wip: start pr-finisher fixes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 58415b7cf82..dc3e3e61aa6 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -440,7 +440,7 @@ async function assignAgentToIssue( return !!response?.replaceActorsForAssignable?.__typename; } catch (error) { const errorMessage = getErrorMessage(error); - const err = /** @type {any} */ (error); + const err = /** @type {any} */ error; const is502Error = err?.response?.status === 502 || errorMessage.includes("502 Bad Gateway"); if (is502Error) { core.warning(`Received 502 error from cloud gateway during agent task creation, but task may have been created`); From d300bde97ae88d82606982ab6ca1aa2023b811a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:33:17 +0000 Subject: [PATCH 5/9] fix: address all review thread issues - unified detection, auth re-throw, permission strings, tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 34 ++++-- .../setup/js/assign_agent_helpers.test.cjs | 102 ++++++++++++++++++ actions/setup/js/pr_helpers.cjs | 5 +- actions/setup/js/pr_helpers.test.cjs | 43 ++++++++ 4 files changed, 175 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index dc3e3e61aa6..603119d12c7 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -42,7 +42,7 @@ function getAgentName(assignee) { * @returns {Promise} */ async function getAvailableAgentLogins(owner, repo, githubClient = github) { - if (!githubClient?.rest?.issues?.checkUserCanBeAssigned && githubClient?.graphql) { + if (!githubClient?.request && githubClient?.graphql) { const query = ` query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { @@ -100,7 +100,7 @@ async function findAgent(owner, repo, agentName, githubClient = github) { return null; } - if (!githubClient?.rest?.issues?.checkUserCanBeAssigned && githubClient?.graphql) { + if (!githubClient?.request && githubClient?.graphql) { const query = ` query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { @@ -157,6 +157,15 @@ async function findAgent(owner, repo, agentName, githubClient = github) { } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to find ${agentName} agent: ${errorMessage}`); + if ( + errorMessage.includes("Bad credentials") || + errorMessage.includes("Not Authenticated") || + errorMessage.includes("Resource not accessible") || + errorMessage.includes("Insufficient permissions") || + errorMessage.includes("requires authentication") + ) { + throw error; + } const available = await getAvailableAgentLogins(owner, repo, githubClient); core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`); if (available.length > 0) { @@ -180,7 +189,7 @@ async function findAgent(owner, repo, agentName, githubClient = github) { * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "issue", number: number}}|null>} */ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) { - if (!githubClient?.rest?.issues?.get && githubClient?.graphql) { + if (!githubClient?.request && githubClient?.graphql) { const query = ` query($owner: String!, $repo: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repo) { @@ -250,7 +259,7 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "pull", number: number}}|null>} */ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = github) { - if (!githubClient?.rest?.pulls?.get && githubClient?.graphql) { + if (!githubClient?.request && githubClient?.graphql) { const query = ` query($owner: String!, $repo: String!, $pullNumber: Int!) { repository(owner: $owner, name: $repo) { @@ -343,7 +352,7 @@ async function assignAgentToIssue( taskContext = null, pullRequestRepoSlug = null ) { - // SECURITY: pullRequestRepoId specifies a cross-repo target repository slug. + // SECURITY: pullRequestRepoSlug specifies a cross-repo target repository slug. // Callers MUST validate the corresponding repository slug against allowedRepos using // validateTargetRepo (from repo_helpers.cjs) before invoking this function. // Filter current assignees based on allowed list (if configured) @@ -452,6 +461,11 @@ async function assignAgentToIssue( } } + if (!githubClient?.request) { + core.error(`GitHub client does not support REST requests; cannot create agent task`); + return false; + } + if (!taskContext) { core.error(`Invalid assignment context: ${assignableId}`); return false; @@ -471,7 +485,10 @@ async function assignAgentToIssue( const targetRepo = targetParts[1]; const promptParts = [`Start work for ${itemType} ${sourceOwner}/${sourceRepo}#${itemNumber}.`, `Use this as the primary context: ${sourceUrl}`]; if (targetRepoSlug !== `${sourceOwner}/${sourceRepo}`) promptParts.push(`Create the branch and pull request in ${targetRepoSlug}.`); - if (customAgent) promptParts.push(`Custom agent: ${customAgent}`); + if (customAgent) { + core.warning(`customAgent is not a dedicated REST parameter; it will be included as prompt context. If the agent runner does not parse this field, the custom agent selection may be ignored.`); + promptParts.push(`Custom agent: ${customAgent}`); + } if (customInstructions) promptParts.push(`Additional instructions:\n${customInstructions}`); const prompt = promptParts.join("\n\n"); @@ -552,8 +569,9 @@ async function assignAgentToIssue( } if ( - errorMessage.includes("Resource not accessible by personal access token") || - errorMessage.includes("Resource not accessible by integration") || + errorMessage.includes("Bad credentials") || + errorMessage.includes("Not Authenticated") || + errorMessage.includes("Resource not accessible") || errorMessage.includes("Insufficient permissions") || errorMessage.includes("requires authentication") ) { diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index 87bd31aea6f..a15554ac2f9 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -484,6 +484,108 @@ describe("assign_agent_helpers.cjs", () => { }); }); + describe("assignAgentToIssue REST task creation path", () => { + const taskContext = { owner: "myorg", repo: "myrepo", type: "issue", number: 42 }; + + it("should start an agent task via REST when githubClient.request is present", async () => { + const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); + const restClient = { request: mockRequest }; + + const result = await assignAgentToIssue("ignored-id", "ignored-agent-id", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + + expect(result).toBe(true); + expect(mockRequest).toHaveBeenCalledWith( + "POST /agents/repos/{owner}/{repo}/tasks", + expect.objectContaining({ + owner: "myorg", + repo: "myrepo", + create_pull_request: true, + prompt: expect.stringContaining("myorg/myrepo#42"), + }) + ); + }); + + it("should include model in REST request when provided", async () => { + const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "agent", [], "copilot", null, null, "claude-opus-4.6", null, null, null, restClient, taskContext); + + expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ model: "claude-opus-4.6" })); + }); + + it("should include base_ref in REST request when baseBranch is provided", async () => { + const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, "develop", restClient, taskContext); + + expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ base_ref: "develop" })); + }); + + it("should not include model or base_ref when not provided", async () => { + const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + + const callArgs = mockRequest.mock.calls[0][1]; + expect(callArgs.model).toBeUndefined(); + expect(callArgs.base_ref).toBeUndefined(); + }); + + it("should use pullRequestRepoSlug as target when provided", async () => { + const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext, "otherorg/otherrepo"); + + expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: "otherorg", repo: "otherrepo" })); + }); + + it("should return false and not call request when taskContext is missing", async () => { + const mockRequest = vi.fn(); + const restClient = { request: mockRequest }; + + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, null); + + expect(result).toBe(false); + expect(mockRequest).not.toHaveBeenCalled(); + expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Invalid assignment context")); + }); + + it("should return false when client has neither request nor graphql", async () => { + const emptyClient = {}; + + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, emptyClient, taskContext); + + expect(result).toBe(false); + expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("does not support REST requests")); + }); + + it("should treat 502 REST errors as success", async () => { + const err502 = Object.assign(new Error("502 Bad Gateway"), { response: { status: 502 } }); + const mockRequest = vi.fn().mockRejectedValue(err502); + const restClient = { request: mockRequest }; + + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + + expect(result).toBe(true); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("502")); + }); + + it("should call logPermissionError for Bad credentials on REST path", async () => { + const errAuth = new Error("Bad credentials"); + const mockRequest = vi.fn().mockRejectedValue(errAuth); + const restClient = { request: mockRequest }; + + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + + expect(result).toBe(false); + expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Insufficient permissions")); + }); + }); + describe("generatePermissionErrorSummary", () => { it("should return markdown content with permission requirements", () => { const summary = generatePermissionErrorSummary(); diff --git a/actions/setup/js/pr_helpers.cjs b/actions/setup/js/pr_helpers.cjs index 0cc3e8617a2..857ae19dd3b 100644 --- a/actions/setup/js/pr_helpers.cjs +++ b/actions/setup/js/pr_helpers.cjs @@ -100,7 +100,10 @@ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) } const { data } = await github.rest.repos.get({ owner, repo }); - const repoId = data.node_id || `${owner}/${repo}`; + const repoId = data.node_id; + if (!repoId) { + throw new Error(`Repository ${owner}/${repo} did not return a valid node_id from the REST API`); + } const resolvedDefaultBranch = data.default_branch ?? null; const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; return { repoId, repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; diff --git a/actions/setup/js/pr_helpers.test.cjs b/actions/setup/js/pr_helpers.test.cjs index 74c080184dc..4d5a1ee6213 100644 --- a/actions/setup/js/pr_helpers.test.cjs +++ b/actions/setup/js/pr_helpers.test.cjs @@ -329,6 +329,49 @@ describe("resolvePullRequestRepo", () => { expect(result.resolvedDefaultBranch).toBeNull(); expect(result.effectiveBaseBranch).toBeNull(); }); + + it("uses REST repos.get when github.request is present", async () => { + const fakeGithub = { + request: vi.fn(), + rest: { + repos: { + get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "main" } }), + }, + }, + }; + const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", undefined); + expect(result.repoId).toBe("MDEwOlJlcG9zaXRvcnkx"); + expect(result.repoSlug).toBe("owner/repo"); + expect(result.resolvedDefaultBranch).toBe("main"); + expect(result.effectiveBaseBranch).toBe("main"); + expect(fakeGithub.rest.repos.get).toHaveBeenCalledWith({ owner: "owner", repo: "repo" }); + }); + + it("explicit configuredBaseBranch overrides REST default_branch", async () => { + const fakeGithub = { + request: vi.fn(), + rest: { + repos: { + get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "main" } }), + }, + }, + }; + const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", "release"); + expect(result.resolvedDefaultBranch).toBe("main"); + expect(result.effectiveBaseBranch).toBe("release"); + }); + + it("throws when REST response is missing node_id", async () => { + const fakeGithub = { + request: vi.fn(), + rest: { + repos: { + get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }), + }, + }, + }; + await expect(resolvePullRequestRepo(fakeGithub, "owner", "repo", undefined)).rejects.toThrow("Repository owner/repo did not return a valid node_id from the REST API"); + }); }); describe("buildBranchInstruction", () => { From e10680cc1edbf0fcbb3e84b9b3dc60110cfc7dc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:02:08 +0000 Subject: [PATCH 6/9] fix: remove GraphQL support completely from assign-to-agent helpers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_agent_helpers.cjs | 230 +------ .../setup/js/assign_agent_helpers.test.cjs | 611 +++++------------- actions/setup/js/assign_to_agent.cjs | 10 +- actions/setup/js/assign_to_agent.test.cjs | 13 +- actions/setup/js/create_pull_request.cjs | 4 +- actions/setup/js/pr_helpers.cjs | 20 +- actions/setup/js/pr_helpers.test.cjs | 32 +- 7 files changed, 185 insertions(+), 735 deletions(-) diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index 603119d12c7..fd3561eedd1 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -42,29 +42,6 @@ function getAgentName(assignee) { * @returns {Promise} */ async function getAvailableAgentLogins(owner, repo, githubClient = github) { - if (!githubClient?.request && githubClient?.graphql) { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { - nodes { ... on Bot { login __typename } } - } - } - } - `; - try { - const response = await githubClient.graphql(query, { owner, repo }); - const actors = response.repository?.suggestedActors?.nodes || []; - const knownValues = Object.values(AGENT_LOGIN_NAMES); - const available = actors.filter(actor => actor?.login && knownValues.includes(actor.login)).map(actor => actor.login); - return available.sort(); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - core.debug(`Failed to list available agent logins: ${errorMessage}`); - return []; - } - } - const knownValues = Object.values(AGENT_LOGIN_NAMES); const available = []; for (const login of knownValues) { @@ -100,52 +77,6 @@ async function findAgent(owner, repo, agentName, githubClient = github) { return null; } - if (!githubClient?.request && githubClient?.graphql) { - const query = ` - query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - suggestedActors(first: 100, capabilities: [CAN_BE_ASSIGNED]) { - nodes { - ... on Bot { - id - login - __typename - } - } - } - } - } - `; - try { - const response = await githubClient.graphql(query, { owner, repo }); - const actors = response.repository.suggestedActors.nodes; - const agent = actors.find(actor => actor.login === loginName); - if (agent) return agent.id; - const knownValues = Object.values(AGENT_LOGIN_NAMES); - const available = actors.filter(a => a?.login && knownValues.includes(a.login)).map(a => a.login); - core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`); - if (available.length > 0) core.info(`Available assignable coding agents: ${available.join(", ")}`); - else core.info("No coding agents are currently assignable in this repository."); - if (agentName === "copilot") { - core.info("Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot"); - } - return null; - } catch (error) { - const errorMessage = getErrorMessage(error); - core.error(`Failed to find ${agentName} agent: ${errorMessage}`); - if ( - errorMessage.includes("Bad credentials") || - errorMessage.includes("Not Authenticated") || - errorMessage.includes("Resource not accessible") || - errorMessage.includes("Insufficient permissions") || - errorMessage.includes("requires authentication") - ) { - throw error; - } - return null; - } - } - try { await githubClient.rest.issues.checkUserCanBeAssigned({ owner, @@ -189,41 +120,6 @@ async function findAgent(owner, repo, agentName, githubClient = github) { * @returns {Promise<{issueId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "issue", number: number}}|null>} */ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) { - if (!githubClient?.request && githubClient?.graphql) { - const query = ` - query($owner: String!, $repo: String!, $issueNumber: Int!) { - repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { - id - assignees(first: 100) { - nodes { - id - login - } - } - } - } - } - `; - try { - const response = await githubClient.graphql(query, { owner, repo, issueNumber }); - const issue = response.repository.issue; - if (!issue || !issue.id) { - core.error("Could not get issue data"); - return null; - } - const currentAssignees = issue.assignees.nodes.map(assignee => ({ - id: assignee.id, - login: assignee.login, - })); - return { issueId: issue.id, currentAssignees }; - } catch (error) { - const errorMessage = getErrorMessage(error); - core.error(`Failed to get issue details: ${errorMessage}`); - throw error; - } - } - try { const { data: issue } = await githubClient.rest.issues.get({ owner, repo, issue_number: issueNumber }); if (!issue || !issue.number) { @@ -259,41 +155,6 @@ async function getIssueDetails(owner, repo, issueNumber, githubClient = github) * @returns {Promise<{pullRequestId: string, currentAssignees: Array<{id: string, login: string}>, htmlUrl: string, title: string, body: string, taskContext: {owner: string, repo: string, type: "pull", number: number}}|null>} */ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = github) { - if (!githubClient?.request && githubClient?.graphql) { - const query = ` - query($owner: String!, $repo: String!, $pullNumber: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $pullNumber) { - id - assignees(first: 100) { - nodes { - id - login - } - } - } - } - } - `; - try { - const response = await githubClient.graphql(query, { owner, repo, pullNumber }); - const pullRequest = response.repository.pullRequest; - if (!pullRequest || !pullRequest.id) { - core.error("Could not get pull request data"); - return null; - } - const currentAssignees = pullRequest.assignees.nodes.map(assignee => ({ - id: assignee.id, - login: assignee.login, - })); - return { pullRequestId: pullRequest.id, currentAssignees }; - } catch (error) { - const errorMessage = getErrorMessage(error); - core.error(`Failed to get pull request details: ${errorMessage}`); - throw error; - } - } - try { const { data: pullRequest } = await githubClient.rest.pulls.get({ owner, repo, pull_number: pullNumber }); if (!pullRequest || !pullRequest.number) { @@ -327,7 +188,6 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git * @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} pullRequestRepoId - Optional pull request repository ID for GraphQL fallback * @param {string|null} model - Optional AI model to use (e.g., "claude-opus-4.6", "auto") * @param {string|null} customAgent - Optional custom agent ID for custom agents * @param {string|null} customInstructions - Optional custom instructions for the agent @@ -343,7 +203,6 @@ async function assignAgentToIssue( currentAssignees, agentName, allowedAgents = null, - pullRequestRepoId = null, model = null, customAgent = null, customInstructions = null, @@ -374,93 +233,6 @@ async function assignAgentToIssue( }); } - if (!githubClient?.request && githubClient?.graphql) { - const actorIds = [agentId, ...filteredAssignees.map(a => a.id).filter(id => id !== agentId)]; - const hasAgentAssignment = pullRequestRepoId || model || customAgent || customInstructions || baseBranch; - let mutation; - let variables; - if (hasAgentAssignment) { - const agentAssignmentFields = []; - const agentAssignmentParams = []; - if (pullRequestRepoId) { - agentAssignmentFields.push("targetRepositoryId: $targetRepoId"); - agentAssignmentParams.push("$targetRepoId: ID!"); - } - if (model) { - agentAssignmentFields.push("model: $model"); - agentAssignmentParams.push("$model: String!"); - } - if (customAgent) { - agentAssignmentFields.push("customAgent: $customAgent"); - agentAssignmentParams.push("$customAgent: String!"); - } - if (customInstructions) { - agentAssignmentFields.push("customInstructions: $customInstructions"); - agentAssignmentParams.push("$customInstructions: String!"); - } - if (baseBranch) { - agentAssignmentFields.push("baseRef: $baseRef"); - agentAssignmentParams.push("$baseRef: String!"); - } - const allParams = ["$assignableId: ID!", "$actorIds: [ID!]!", ...agentAssignmentParams].join(", "); - const assignmentFields = agentAssignmentFields.join("\n "); - mutation = ` - mutation(${allParams}) { - replaceActorsForAssignable(input: { - assignableId: $assignableId, - actorIds: $actorIds, - agentAssignment: { - ${assignmentFields} - } - }) { - __typename - } - } - `; - variables = { - assignableId, - actorIds, - ...(pullRequestRepoId && { targetRepoId: pullRequestRepoId }), - ...(model && { model }), - ...(customAgent && { customAgent }), - ...(customInstructions && { customInstructions }), - ...(baseBranch && { baseRef: baseBranch }), - }; - } else { - mutation = ` - mutation($assignableId: ID!, $actorIds: [ID!]!) { - replaceActorsForAssignable(input: { - assignableId: $assignableId, - actorIds: $actorIds - }) { - __typename - } - } - `; - variables = { assignableId, actorIds }; - } - try { - const response = await githubClient.graphql(mutation, { - ...variables, - headers: { - "GraphQL-Features": "issues_copilot_assignment_api_support,coding_agent_model_selection", - }, - }); - return !!response?.replaceActorsForAssignable?.__typename; - } catch (error) { - const errorMessage = getErrorMessage(error); - const err = /** @type {any} */ error; - const is502Error = err?.response?.status === 502 || errorMessage.includes("502 Bad Gateway"); - if (is502Error) { - core.warning(`Received 502 error from cloud gateway during agent task creation, but task may have been created`); - core.info(`Treating 502 error as success - agent task likely created`); - return true; - } - core.error(`Failed to assign ${agentName}: ${errorMessage}`); - return false; - } - } - if (!githubClient?.request) { core.error(`GitHub client does not support REST requests; cannot create agent task`); return false; @@ -691,7 +463,7 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) { // Assign agent by starting a REST task (no allowed list filtering in this helper) core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); - const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null, null, null, null, null, null, github, issueDetails.taskContext); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null, null, null, null, null, github, issueDetails.taskContext); if (!success) { return { success: false, error: `Failed to assign ${agentName} via REST` }; diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index a15554ac2f9..b54ff9eff10 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -10,7 +10,19 @@ const mockCore = { }; const mockGithub = { - graphql: vi.fn(), + request: vi.fn(), + rest: { + issues: { + checkUserCanBeAssigned: vi.fn(), + get: vi.fn(), + }, + users: { + getByUsername: vi.fn(), + }, + pulls: { + get: vi.fn(), + }, + }, }; // Set up global mocks before importing the module @@ -58,77 +70,45 @@ describe("assign_agent_helpers.cjs", () => { }); describe("getAvailableAgentLogins", () => { - it("should return available agent logins using github.graphql when no token provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [ - { login: "copilot-swe-agent", __typename: "Bot" }, - { login: "some-other-bot", __typename: "Bot" }, - ], - }, - }, - }); + it("should return available agent logins when checkUserCanBeAssigned succeeds", async () => { + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); const result = await getAvailableAgentLogins("owner", "repo"); expect(result).toEqual(["copilot-swe-agent"]); - expect(mockGithub.graphql).toHaveBeenCalledTimes(1); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledWith({ owner: "owner", repo: "repo", assignee: "copilot-swe-agent" }); }); - it("should return empty array when no agents are available", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "some-random-bot", __typename: "Bot" }], - }, - }, - }); + it("should return empty array when agent is not assignable (404)", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); const result = await getAvailableAgentLogins("owner", "repo"); expect(result).toEqual([]); }); - it("should handle GraphQL errors gracefully", async () => { - mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error")); - - const result = await getAvailableAgentLogins("owner", "repo"); - - expect(result).toEqual([]); - expect(mockCore.debug).toHaveBeenCalledWith(expect.stringContaining("Failed to list available agent logins")); - }); - - it("should handle null suggestedActors", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: null, - }, - }); + it("should handle non-404 errors gracefully and return empty array", async () => { + const err500 = Object.assign(new Error("Server error"), { status: 500 }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err500); const result = await getAvailableAgentLogins("owner", "repo"); expect(result).toEqual([]); + expect(mockCore.debug).toHaveBeenCalledWith(expect.stringContaining("Failed to check assignability for copilot-swe-agent")); }); }); describe("findAgent", () => { - it("should find copilot agent and return its ID using github.graphql", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [ - { id: "BOT_12345", login: "copilot-swe-agent", __typename: "Bot" }, - { id: "BOT_67890", login: "other-bot", __typename: "Bot" }, - ], - }, - }, - }); + it("should find copilot agent and return its ID via REST", async () => { + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 12345 } }); const result = await findAgent("owner", "repo", "copilot"); - expect(result).toBe("BOT_12345"); - expect(mockGithub.graphql).toHaveBeenCalledTimes(1); + expect(result).toBe("12345"); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledWith({ owner: "owner", repo: "repo", assignee: "copilot-swe-agent" }); + expect(mockGithub.rest.users.getByUsername).toHaveBeenCalledWith({ username: "copilot-swe-agent" }); }); it("should return null for unknown agent name", async () => { @@ -138,14 +118,11 @@ describe("assign_agent_helpers.cjs", () => { expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Unknown agent: unknown-agent")); }); - it("should return null when copilot is not available", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ id: "BOT_67890", login: "other-bot", __typename: "Bot" }], - }, - }, - }); + it("should return null when copilot is not available (404)", async () => { + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); + // getAvailableAgentLogins also calls checkUserCanBeAssigned + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); const result = await findAgent("owner", "repo", "copilot"); @@ -153,8 +130,18 @@ describe("assign_agent_helpers.cjs", () => { expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("copilot coding agent (copilot-swe-agent) is not available")); }); - it("should handle GraphQL errors", async () => { - mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error")); + it("should handle REST errors and re-throw auth errors", async () => { + const authError = new Error("Bad credentials"); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(authError); + + await expect(findAgent("owner", "repo", "copilot")).rejects.toThrow("Bad credentials"); + }); + + it("should return null for non-auth errors", async () => { + const err = new Error("Something unexpected"); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err); + // getAvailableAgentLogins fallback + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(Object.assign(new Error("Not Found"), { status: 404 })); const result = await findAgent("owner", "repo", "copilot"); @@ -164,38 +151,38 @@ describe("assign_agent_helpers.cjs", () => { }); describe("getIssueDetails", () => { - it("should return issue ID and current assignees", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_123", - assignees: { - nodes: [ - { id: "USER_1", login: "user1" }, - { id: "USER_2", login: "user2" }, - ], - }, - }, + it("should return issue ID, current assignees, and task context", async () => { + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { + id: 67890, + number: 123, + assignees: [ + { id: 1, login: "user1" }, + { id: 2, login: "user2" }, + ], + html_url: "https://github.com/owner/repo/issues/123", + title: "Test issue", + body: "Test body", }, }); const result = await getIssueDetails("owner", "repo", 123); expect(result).toEqual({ - issueId: "ISSUE_123", + issueId: "67890", currentAssignees: [ - { id: "USER_1", login: "user1" }, - { id: "USER_2", login: "user2" }, + { id: "1", login: "user1" }, + { id: "2", login: "user2" }, ], + htmlUrl: "https://github.com/owner/repo/issues/123", + title: "Test issue", + body: "Test body", + taskContext: { owner: "owner", repo: "repo", type: "issue", number: 123 }, }); }); it("should return null when issue is not found", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - issue: null, - }, - }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: null }); const result = await getIssueDetails("owner", "repo", 999); @@ -203,295 +190,46 @@ describe("assign_agent_helpers.cjs", () => { expect(mockCore.error).toHaveBeenCalledWith("Could not get issue data"); }); - it("should handle GraphQL errors", async () => { - mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error")); + it("should re-throw REST errors", async () => { + mockGithub.rest.issues.get.mockRejectedValueOnce(new Error("API error")); - await expect(getIssueDetails("owner", "repo", 123)).rejects.toThrow("GraphQL error"); + await expect(getIssueDetails("owner", "repo", 123)).rejects.toThrow("API error"); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to get issue details")); }); it("should return empty assignees when none exist", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_123", - assignees: { - nodes: [], - }, - }, + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { + id: 67890, + number: 123, + assignees: [], + html_url: "https://github.com/owner/repo/issues/123", + title: "Test issue", + body: "", }, }); const result = await getIssueDetails("owner", "repo", 123); expect(result).toEqual({ - issueId: "ISSUE_123", + issueId: "67890", currentAssignees: [], + htmlUrl: "https://github.com/owner/repo/issues/123", + title: "Test issue", + body: "", + taskContext: { owner: "owner", repo: "repo", type: "issue", number: 123 }, }); }); }); describe("assignAgentToIssue", () => { - it("should successfully assign agent using mutation", async () => { - // Mock the global github.graphql - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - const result = await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null); - - expect(result).toBe(true); - expect(mockGithub.graphql).toHaveBeenCalledWith( - expect.stringContaining("replaceActorsForAssignable"), - expect.objectContaining({ - assignableId: "ISSUE_123", - actorIds: ["AGENT_456", "USER_1"], - }) - ); - - // Should always include both issues_copilot_assignment_api_support and coding_agent_model_selection - const calledArgs = mockGithub.graphql.mock.calls[0]; - const variables = calledArgs[1]; - expect(variables.headers["GraphQL-Features"]).toBe("issues_copilot_assignment_api_support,coding_agent_model_selection"); - }); - - it("should preserve existing assignees when adding agent", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue( - "ISSUE_123", - "AGENT_456", - [ - { id: "USER_1", login: "user1" }, - { id: "USER_2", login: "user2" }, - ], - "copilot", - null - ); - - expect(mockGithub.graphql).toHaveBeenCalledWith( - expect.stringContaining("replaceActorsForAssignable"), - expect.objectContaining({ - assignableId: "ISSUE_123", - actorIds: expect.arrayContaining(["AGENT_456", "USER_1", "USER_2"]), - }) - ); - }); - - it("should not duplicate agent if already in assignees", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue( - "ISSUE_123", - "AGENT_456", - ["AGENT_456", "USER_1"], // Agent already in list - "copilot" - ); - - const calledArgs = mockGithub.graphql.mock.calls[0][1]; - // Agent should only appear once in the actorIds array - const agentMatches = calledArgs.actorIds.filter(id => id === "AGENT_456"); - expect(agentMatches.length).toBe(1); - }); - - it("should include model in agentAssignment when provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, "claude-opus-4.6"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - // Mutation should include agentAssignment with model - expect(mutation).toContain("agentAssignment"); - expect(mutation).toContain("model: $model"); - expect(variables.model).toBe("claude-opus-4.6"); - // Should always include both feature flags - expect(variables.headers["GraphQL-Features"]).toBe("issues_copilot_assignment_api_support,coding_agent_model_selection"); - }); - - it("should include customAgent in agentAssignment when provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, null, "custom-agent-123"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - // Mutation should include agentAssignment with customAgent - expect(mutation).toContain("agentAssignment"); - expect(mutation).toContain("customAgent: $customAgent"); - expect(variables.customAgent).toBe("custom-agent-123"); - }); - - it("should include customInstructions in agentAssignment when provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, null, null, "Focus on performance optimization"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - // Mutation should include agentAssignment with customInstructions - expect(mutation).toContain("agentAssignment"); - expect(mutation).toContain("customInstructions: $customInstructions"); - expect(variables.customInstructions).toBe("Focus on performance optimization"); - }); - - it("should include multiple agentAssignment parameters when provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, "REPO_ID_789", "claude-opus-4.6", "custom-agent-123", "Focus on performance"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - // Mutation should include agentAssignment with all parameters - expect(mutation).toContain("agentAssignment"); - expect(mutation).toContain("targetRepositoryId: $targetRepoId"); - expect(mutation).toContain("model: $model"); - expect(mutation).toContain("customAgent: $customAgent"); - expect(mutation).toContain("customInstructions: $customInstructions"); - - expect(variables.targetRepoId).toBe("REPO_ID_789"); - expect(variables.model).toBe("claude-opus-4.6"); - expect(variables.customAgent).toBe("custom-agent-123"); - expect(variables.customInstructions).toBe("Focus on performance"); - }); - - it("should include baseBranch as baseRef in agentAssignment when provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, null, null, null, "develop"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - // Mutation should include agentAssignment with baseRef - expect(mutation).toContain("agentAssignment"); - expect(mutation).toContain("baseRef: $baseRef"); - expect(variables.baseRef).toBe("develop"); - }); - - it("should not include baseRef in mutation when baseBranch is not provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, null, null, null, null); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - expect(mutation).not.toContain("baseRef"); - expect(variables.baseRef).toBeUndefined(); - }); - - it("should include baseRef alongside other agentAssignment parameters", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, "REPO_ID_789", "claude-opus-4.6", null, "Fix the bug", "develop"); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - const variables = calledArgs[1]; - - expect(mutation).toContain("targetRepositoryId: $targetRepoId"); - expect(mutation).toContain("model: $model"); - expect(mutation).toContain("customInstructions: $customInstructions"); - expect(mutation).toContain("baseRef: $baseRef"); - expect(variables.targetRepoId).toBe("REPO_ID_789"); - expect(variables.model).toBe("claude-opus-4.6"); - expect(variables.customInstructions).toBe("Fix the bug"); - expect(variables.baseRef).toBe("develop"); - }); - - it("should omit agentAssignment when no agent-specific parameters provided", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, null, null, null); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const mutation = calledArgs[0]; - - // Mutation should NOT include agentAssignment when no parameters provided - expect(mutation).not.toContain("agentAssignment"); - }); - - it("should include only provided agentAssignment fields", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); - - // Only provide model, not customAgent or customInstructions - await assignAgentToIssue("ISSUE_123", "AGENT_456", [{ id: "USER_1", login: "user1" }], "copilot", null, null, "claude-opus-4.6", null, null); - - const calledArgs = mockGithub.graphql.mock.calls[0]; - const variables = calledArgs[1]; - - // Only model should be in variables - expect(variables.model).toBe("claude-opus-4.6"); - expect(variables.customAgent).toBeUndefined(); - expect(variables.customInstructions).toBeUndefined(); - }); - }); - - describe("assignAgentToIssue REST task creation path", () => { const taskContext = { owner: "myorg", repo: "myrepo", type: "issue", number: 42 }; - it("should start an agent task via REST when githubClient.request is present", async () => { + it("should start an agent task via REST", async () => { const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const restClient = { request: mockRequest }; - const result = await assignAgentToIssue("ignored-id", "ignored-agent-id", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + const result = await assignAgentToIssue("ignored-id", "ignored-agent-id", [], "copilot", null, null, null, null, null, restClient, taskContext); expect(result).toBe(true); expect(mockRequest).toHaveBeenCalledWith( @@ -509,7 +247,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const restClient = { request: mockRequest }; - await assignAgentToIssue("id", "agent", [], "copilot", null, null, "claude-opus-4.6", null, null, null, restClient, taskContext); + await assignAgentToIssue("id", "agent", [], "copilot", null, "claude-opus-4.6", null, null, null, restClient, taskContext); expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ model: "claude-opus-4.6" })); }); @@ -518,7 +256,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const restClient = { request: mockRequest }; - await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, "develop", restClient, taskContext); + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, "develop", restClient, taskContext); expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ base_ref: "develop" })); }); @@ -527,7 +265,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const restClient = { request: mockRequest }; - await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, restClient, taskContext); const callArgs = mockRequest.mock.calls[0][1]; expect(callArgs.model).toBeUndefined(); @@ -538,7 +276,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const restClient = { request: mockRequest }; - await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext, "otherorg/otherrepo"); + await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, restClient, taskContext, "otherorg/otherrepo"); expect(mockRequest).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: "otherorg", repo: "otherrepo" })); }); @@ -547,7 +285,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn(); const restClient = { request: mockRequest }; - const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, null); + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, restClient, null); expect(result).toBe(false); expect(mockRequest).not.toHaveBeenCalled(); @@ -557,7 +295,7 @@ describe("assign_agent_helpers.cjs", () => { it("should return false when client has neither request nor graphql", async () => { const emptyClient = {}; - const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, emptyClient, taskContext); + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, emptyClient, taskContext); expect(result).toBe(false); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("does not support REST requests")); @@ -568,7 +306,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockRejectedValue(err502); const restClient = { request: mockRequest }; - const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, restClient, taskContext); expect(result).toBe(true); expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("502")); @@ -579,7 +317,7 @@ describe("assign_agent_helpers.cjs", () => { const mockRequest = vi.fn().mockRejectedValue(errAuth); const restClient = { request: mockRequest }; - const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, null, restClient, taskContext); + const result = await assignAgentToIssue("id", "agent", [], "copilot", null, null, null, null, null, restClient, taskContext); expect(result).toBe(false); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Insufficient permissions")); @@ -600,39 +338,21 @@ describe("assign_agent_helpers.cjs", () => { describe("assignAgentToIssueByName", () => { it("should successfully assign copilot agent", async () => { - // Mock findAgent (uses github.graphql) - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ id: "AGENT_456", login: "copilot-swe-agent", __typename: "Bot" }], - }, - }, - }); - - // Mock getIssueDetails (uses github.graphql) - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_123", - assignees: { - nodes: [], - }, - }, - }, - }); - - // Mock assignAgentToIssue mutation (uses github.graphql) - mockGithub.graphql.mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, + // findAgent: checkUserCanBeAssigned + getByUsername + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); + // getIssueDetails + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 1111, number: 123, assignees: [], html_url: "", title: "", body: "" }, }); + // assignAgentToIssue (REST) + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); const result = await assignAgentToIssueByName("owner", "repo", 123, "copilot"); expect(result.success).toBe(true); expect(mockCore.info).toHaveBeenCalledWith("Looking for copilot coding agent..."); - expect(mockCore.info).toHaveBeenCalledWith("Found copilot coding agent (ID: AGENT_456)"); + expect(mockCore.info).toHaveBeenCalledWith("Found copilot coding agent (ID: 999)"); }); it("should return error for unsupported agent", async () => { @@ -644,15 +364,11 @@ describe("assign_agent_helpers.cjs", () => { }); it("should return error when agent is not available", async () => { - // Mock findAgent and getAvailableAgentLogins (both use github.graphql) - // Both calls return empty nodes - mockGithub.graphql.mockResolvedValue({ - repository: { - suggestedActors: { - nodes: [], // No agents - }, - }, - }); + const err404 = Object.assign(new Error("Not Found"), { status: 404 }); + // findAgent: checkUserCanBeAssigned throws 404 + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); + // getAvailableAgentLogins: also 404 + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(err404); const result = await assignAgentToIssueByName("owner", "repo", 123, "copilot"); @@ -661,26 +377,18 @@ describe("assign_agent_helpers.cjs", () => { }); it("should report already assigned when agent is in assignees", async () => { - const agentId = "AGENT_456"; - - // Mock findAgent (uses github.graphql) - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ id: agentId, login: "copilot-swe-agent", __typename: "Bot" }], - }, - }, - }); - - // Mock getIssueDetails (uses github.graphql) - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_123", - assignees: { - nodes: [{ id: agentId }], // Already assigned - }, - }, + // findAgent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 999 } }); + // getIssueDetails - agent already assigned + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { + id: 1111, + number: 123, + assignees: [{ id: 999, login: "copilot-swe-agent" }], + html_url: "", + title: "", + body: "", }, }); @@ -692,38 +400,38 @@ describe("assign_agent_helpers.cjs", () => { }); describe("getPullRequestDetails", () => { - it("should return pull request ID and current assignees", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - pullRequest: { - id: "PR_123", - assignees: { - nodes: [ - { id: "USER_1", login: "user1" }, - { id: "USER_2", login: "user2" }, - ], - }, - }, + it("should return pull request ID, current assignees, and task context", async () => { + mockGithub.rest.pulls.get.mockResolvedValueOnce({ + data: { + id: 67890, + number: 42, + assignees: [ + { id: 1, login: "user1" }, + { id: 2, login: "user2" }, + ], + html_url: "https://github.com/owner/repo/pull/42", + title: "Test PR", + body: "Test body", }, }); const result = await getPullRequestDetails("owner", "repo", 42); expect(result).toEqual({ - pullRequestId: "PR_123", + pullRequestId: "67890", currentAssignees: [ - { id: "USER_1", login: "user1" }, - { id: "USER_2", login: "user2" }, + { id: "1", login: "user1" }, + { id: "2", login: "user2" }, ], + htmlUrl: "https://github.com/owner/repo/pull/42", + title: "Test PR", + body: "Test body", + taskContext: { owner: "owner", repo: "repo", type: "pull", number: 42 }, }); }); it("should return null when pull request is not found", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - pullRequest: null, - }, - }); + mockGithub.rest.pulls.get.mockResolvedValueOnce({ data: null }); const result = await getPullRequestDetails("owner", "repo", 999); @@ -732,45 +440,34 @@ describe("assign_agent_helpers.cjs", () => { }); it("should return empty assignees when none exist", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - pullRequest: { - id: "PR_456", - assignees: { - nodes: [], - }, - }, + mockGithub.rest.pulls.get.mockResolvedValueOnce({ + data: { + id: 67890, + number: 42, + assignees: [], + html_url: "https://github.com/owner/repo/pull/42", + title: "Test PR", + body: "", }, }); const result = await getPullRequestDetails("owner", "repo", 42); expect(result).toEqual({ - pullRequestId: "PR_456", + pullRequestId: "67890", currentAssignees: [], + htmlUrl: "https://github.com/owner/repo/pull/42", + title: "Test PR", + body: "", + taskContext: { owner: "owner", repo: "repo", type: "pull", number: 42 }, }); }); - it("should handle GraphQL errors by re-throwing", async () => { - mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error")); + it("should re-throw REST errors", async () => { + mockGithub.rest.pulls.get.mockRejectedValueOnce(new Error("API error")); - await expect(getPullRequestDetails("owner", "repo", 42)).rejects.toThrow("GraphQL error"); + await expect(getPullRequestDetails("owner", "repo", 42)).rejects.toThrow("API error"); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to get pull request details")); }); - - it("should call GraphQL with correct variables", async () => { - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - pullRequest: { - id: "PR_789", - assignees: { nodes: [] }, - }, - }, - }); - - await getPullRequestDetails("myorg", "myrepo", 77); - - expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("pullRequest(number: $pullNumber)"), expect.objectContaining({ owner: "myorg", repo: "myrepo", pullNumber: 77 })); - }); }); }); diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 63fe066a854..0ff389bdff4 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -95,7 +95,6 @@ async function main(config = {}) { // Resolve pull request repo upfront (if globally configured) let pullRequestOwner = null; let pullRequestRepo = null; - let pullRequestRepoId = null; let effectiveBaseBranch = configuredBaseBranch; const pullRequestRepoConfig = config["pull-request-repo"] ? String(config["pull-request-repo"]).trim() : null; @@ -111,14 +110,12 @@ async function main(config = {}) { core.info(`Using pull request repository: ${pullRequestOwner}/${pullRequestRepo}`); try { const resolved = await resolvePullRequestRepo(githubClient, pullRequestOwner, pullRequestRepo, configuredBaseBranch); - pullRequestRepoId = resolved.repoId; effectiveBaseBranch = resolved.effectiveBaseBranch; - core.info(`Pull request repository ID: ${pullRequestRepoId}`); if (!configuredBaseBranch && effectiveBaseBranch) { core.info(`Resolved pull request repository default branch: ${effectiveBaseBranch}`); } } catch (error) { - throw new Error(`Failed to fetch pull request repository ID for ${pullRequestOwner}/${pullRequestRepo}: ${getErrorMessage(error)}`); + throw new Error(`Failed to fetch pull request repository for ${pullRequestOwner}/${pullRequestRepo}: ${getErrorMessage(error)}`); } } else { core.warning(`Invalid pull-request-repo format: ${pullRequestRepoConfig}. Expected owner/repo. PRs will be created in issue repository.`); @@ -240,7 +237,6 @@ async function main(config = {}) { const basePullRequestRepoSlug = pullRequestOwner && pullRequestRepo ? `${pullRequestOwner}/${pullRequestRepo}` : `${effectiveOwner}/${effectiveRepo}`; // Handle per-item pull_request_repo override - let effectivePullRequestRepoId = pullRequestRepoId; let effectivePullRequestRepoSlug = basePullRequestRepoSlug; let hasValidatedPerItemPullRequestRepoOverride = false; const hasPullRequestRepoOverrideField = message.pull_request_repo != null; @@ -258,8 +254,7 @@ async function main(config = {}) { return { success: false, error }; } try { - const resolved = await resolvePullRequestRepo(githubClient, pullRequestRepoParts[0], pullRequestRepoParts[1], configuredBaseBranch); - effectivePullRequestRepoId = resolved.repoId; + await resolvePullRequestRepo(githubClient, pullRequestRepoParts[0], pullRequestRepoParts[1], configuredBaseBranch); effectivePullRequestRepoSlug = itemPullRequestRepo; hasValidatedPerItemPullRequestRepoOverride = true; core.info(`Using per-item pull request repository: ${itemPullRequestRepo}`); @@ -394,7 +389,6 @@ async function main(config = {}) { currentAssignees, agentName, allowedAgents, - effectivePullRequestRepoId, model, customAgent, customInstructions, diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index a2abe043e6a..7e05849014b 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -23,10 +23,21 @@ const mockContext = { }; const mockGithub = { - graphql: vi.fn(), + request: vi.fn().mockResolvedValue({ data: { id: "task-123" } }), rest: { issues: { createComment: vi.fn().mockResolvedValue({ data: { id: 12345 } }), + checkUserCanBeAssigned: vi.fn().mockResolvedValue({}), + get: vi.fn(), + }, + users: { + getByUsername: vi.fn().mockResolvedValue({ data: { id: 99999 } }), + }, + pulls: { + get: vi.fn(), + }, + repos: { + get: vi.fn().mockResolvedValue({ data: { node_id: "REPO_NODE_ID", default_branch: "main" } }), }, }, }; diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index cc72b66ca83..2a39f80ad5d 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -696,12 +696,12 @@ async function main(config = {}) { issueDetails.currentAssignees, "copilot", null, // allowedAgents — not restricted for fallback issues - null, // pullRequestRepoId — not applicable (issue, not PR) null, // model — not applicable null, // customAgent — not applicable null, // customInstructions — not applicable null, // baseBranch — not applicable - copilotClient + copilotClient, + issueDetails.taskContext ); if (assigned) { core.info(`Successfully assigned copilot coding agent to fallback issue #${issueNumber}`); diff --git a/actions/setup/js/pr_helpers.cjs b/actions/setup/js/pr_helpers.cjs index 857ae19dd3b..46442fa5733 100644 --- a/actions/setup/js/pr_helpers.cjs +++ b/actions/setup/js/pr_helpers.cjs @@ -80,25 +80,9 @@ function getPullRequestNumber(messageItem, context) { * @param {string} owner * @param {string} repo * @param {string|null|undefined} configuredBaseBranch - explicitly configured base branch (may be null or undefined) - * @returns {Promise<{repoId: string, repoSlug: string, effectiveBaseBranch: string|null, resolvedDefaultBranch: string|null}>} + * @returns {Promise<{repoSlug: string, effectiveBaseBranch: string|null, resolvedDefaultBranch: string|null}>} */ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) { - if (!github?.rest?.repos?.get && github?.graphql) { - const query = ` - query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - id - defaultBranchRef { name } - } - } - `; - const response = await github.graphql(query, { owner, name: repo }); - const repoId = response.repository.id; - const resolvedDefaultBranch = response.repository.defaultBranchRef?.name ?? null; - const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; - return { repoId, repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; - } - const { data } = await github.rest.repos.get({ owner, repo }); const repoId = data.node_id; if (!repoId) { @@ -106,7 +90,7 @@ async function resolvePullRequestRepo(github, owner, repo, configuredBaseBranch) } const resolvedDefaultBranch = data.default_branch ?? null; const effectiveBaseBranch = configuredBaseBranch || resolvedDefaultBranch; - return { repoId, repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; + return { repoSlug: `${owner}/${repo}`, effectiveBaseBranch, resolvedDefaultBranch }; } /** diff --git a/actions/setup/js/pr_helpers.test.cjs b/actions/setup/js/pr_helpers.test.cjs index 4d5a1ee6213..07f535662c6 100644 --- a/actions/setup/js/pr_helpers.test.cjs +++ b/actions/setup/js/pr_helpers.test.cjs @@ -299,12 +299,15 @@ describe("pr_helpers.cjs", () => { describe("resolvePullRequestRepo", () => { const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); - it("returns repoId, effectiveBaseBranch from explicit config, and resolvedDefaultBranch", async () => { + it("returns effectiveBaseBranch from explicit config and resolvedDefaultBranch", async () => { const fakeGithub = { - graphql: vi.fn().mockResolvedValue({ repository: { id: "repo-id", defaultBranchRef: { name: "develop" } } }), + rest: { + repos: { + get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "develop" } }), + }, + }, }; const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", "feature"); - expect(result.repoId).toBe("repo-id"); expect(result.resolvedDefaultBranch).toBe("develop"); // explicit config wins over fetched default expect(result.effectiveBaseBranch).toBe("feature"); @@ -312,27 +315,19 @@ describe("resolvePullRequestRepo", () => { it("falls back to repo default branch when no explicit base branch configured", async () => { const fakeGithub = { - graphql: vi.fn().mockResolvedValue({ repository: { id: "repo-id", defaultBranchRef: { name: "trunk" } } }), + rest: { + repos: { + get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "trunk" } }), + }, + }, }; const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", undefined); - expect(result.repoId).toBe("repo-id"); expect(result.resolvedDefaultBranch).toBe("trunk"); expect(result.effectiveBaseBranch).toBe("trunk"); }); - it("handles missing defaultBranchRef gracefully", async () => { - const fakeGithub = { - graphql: vi.fn().mockResolvedValue({ repository: { id: "repo-id", defaultBranchRef: null } }), - }; - const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", undefined); - expect(result.repoId).toBe("repo-id"); - expect(result.resolvedDefaultBranch).toBeNull(); - expect(result.effectiveBaseBranch).toBeNull(); - }); - - it("uses REST repos.get when github.request is present", async () => { + it("uses REST repos.get and returns repoSlug", async () => { const fakeGithub = { - request: vi.fn(), rest: { repos: { get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "main" } }), @@ -340,7 +335,6 @@ describe("resolvePullRequestRepo", () => { }, }; const result = await resolvePullRequestRepo(fakeGithub, "owner", "repo", undefined); - expect(result.repoId).toBe("MDEwOlJlcG9zaXRvcnkx"); expect(result.repoSlug).toBe("owner/repo"); expect(result.resolvedDefaultBranch).toBe("main"); expect(result.effectiveBaseBranch).toBe("main"); @@ -349,7 +343,6 @@ describe("resolvePullRequestRepo", () => { it("explicit configuredBaseBranch overrides REST default_branch", async () => { const fakeGithub = { - request: vi.fn(), rest: { repos: { get: vi.fn().mockResolvedValue({ data: { node_id: "MDEwOlJlcG9zaXRvcnkx", default_branch: "main" } }), @@ -363,7 +356,6 @@ describe("resolvePullRequestRepo", () => { it("throws when REST response is missing node_id", async () => { const fakeGithub = { - request: vi.fn(), rest: { repos: { get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }), From 28b3d3c20d5fec8aafd4089646e516e8dbf745dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:08:30 +0000 Subject: [PATCH 7/9] fix: rewrite tests for REST-only assign-to-agent (no GraphQL) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 15 +- actions/setup/js/assign_to_agent.test.cjs | 1091 +++++------------ actions/setup/js/create_pull_request.test.cjs | 48 +- 3 files changed, 317 insertions(+), 837 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 0ff389bdff4..f903db34587 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -383,20 +383,7 @@ async function main(config = {}) { if (customInstructions) core.info(`Using custom instructions: ${customInstructions.substring(0, 100)}${customInstructions.length > 100 ? "..." : ""}`); if (effectiveBaseBranch) core.info(`Using base branch: ${effectiveBaseBranch}`); - const success = await assignAgentToIssue( - assignableId, - agentId, - currentAssignees, - agentName, - allowedAgents, - model, - customAgent, - customInstructions, - effectiveBaseBranch, - githubClient, - taskContext, - effectivePullRequestRepoSlug - ); + const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, model, customAgent, customInstructions, effectiveBaseBranch, githubClient, taskContext, effectivePullRequestRepoSlug); if (!success) throw new Error(`Failed to assign ${agentName} via REST`); core.info(`Successfully assigned ${agentName} coding agent to ${type} #${number}`); diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index 7e05849014b..5c02e2a4fde 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -97,7 +97,7 @@ describe("assign_to_agent", () => { `; const setAgentOutput = data => { - tempFilePath = path.join("/tmp", `test_agent_output_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); + tempFilePath = path.join(process.cwd(), `.test_agent_output_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); const content = typeof data === "string" ? data : JSON.stringify(data); fs.writeFileSync(tempFilePath, content); process.env.GH_AW_AGENT_OUTPUT = tempFilePath; @@ -107,8 +107,13 @@ describe("assign_to_agent", () => { vi.clearAllMocks(); mockSleep.mockClear(); - // Reset mockGithub.graphql to ensure no lingering mock implementations - mockGithub.graphql = vi.fn(); + // Reset REST mocks to default implementations (vi.clearAllMocks() cleared them) + mockGithub.request.mockResolvedValue({ data: { id: "task-123" } }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValue({}); + mockGithub.rest.issues.get.mockReset(); + mockGithub.rest.users.getByUsername.mockResolvedValue({ data: { id: 99999 } }); + mockGithub.rest.pulls.get.mockReset(); + mockGithub.rest.repos.get.mockResolvedValue({ data: { node_id: "REPO_NODE_ID", default_branch: "main" } }); // Reset mockGithub.rest.issues.createComment mockGithub.rest.issues.createComment = vi.fn().mockResolvedValue({ data: { id: 12345 } }); @@ -177,7 +182,7 @@ describe("assign_to_agent", () => { await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); - expect(mockGithub.graphql).not.toHaveBeenCalled(); + expect(mockGithub.request).not.toHaveBeenCalled(); expect(mockCore.summary.addRaw).toHaveBeenCalled(); const summaryCall = mockCore.summary.addRaw.mock.calls[0][0]; expect(summaryCall).toContain("🎭 Staged Mode"); @@ -197,35 +202,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [ - { - login: "copilot-swe-agent", - id: "MDQ6VXNlcjE=", - }, - ], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [], - }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses: findAgent, getIssueDetails, assignAgentToIssue + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -243,35 +226,17 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses for 2 assignments - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-1", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-2", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses for 2 assignments (agent cached after first findAgent) + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 11111, number: 1, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 22222, number: 2, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-2" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -294,38 +259,20 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses: findAgent -> getIssueDetails (issueNumber 99) -> addAssignees - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id-99", - assignees: { nodes: [] }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses: findAgent -> getIssueDetails (issueNumber 99) -> assignAgentToIssue + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 99, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-99" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary issue id")); // Ensure the issue lookup used the resolved issue number - const secondCallArgs = mockGithub.graphql.mock.calls[1]; - expect(secondCallArgs).toBeDefined(); - const variables = secondCallArgs[1]; - expect(variables.issueNumber).toBe(99); + expect(mockGithub.rest.issues.get).toHaveBeenCalledWith(expect.objectContaining({ issue_number: 99 })); }); it("should resolve temporary issue IDs with '#' prefix (#aw_...) using GH_AW_TEMPORARY_ID_MAP", async () => { @@ -344,36 +291,19 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id-99", - assignees: { nodes: [] }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses: findAgent -> getIssueDetails (issueNumber 99) -> assignAgentToIssue + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 99, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-99" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary issue id")); - const secondCallArgs = mockGithub.graphql.mock.calls[1]; - expect(secondCallArgs).toBeDefined(); - const variables = secondCallArgs[1]; - expect(variables.issueNumber).toBe(99); + expect(mockGithub.rest.issues.get).toHaveBeenCalledWith(expect.objectContaining({ issue_number: 99 })); }); it("should defer when issue_number is a '#aw_' temporary ID not yet in map", async () => { @@ -401,7 +331,7 @@ describe("assign_to_agent", () => { })()`); expect(deferred).toMatchObject({ success: false, deferred: true }); - expect(mockGithub.graphql).not.toHaveBeenCalled(); + expect(mockGithub.request).not.toHaveBeenCalled(); }); it("should reject unsupported agents", async () => { @@ -452,25 +382,12 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - agent already assigned - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "MDQ6VXNlcjE=" }], - }, - }, - }, - }); + // Mock REST responses - agent already assigned + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -492,46 +409,20 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - // Get global PR repository ID and default branch - .mockResolvedValueOnce({ - repository: { - id: "default-pr-repo-id", - defaultBranchRef: { name: "main" }, - }, - }) - // Get per-item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "other-platform-repo-id", - }, - }) - // Find agent - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], - }, - }, - }) - // Get issue details - agent is already assigned - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], - }, - }, - }, - }) - // Assign agent (should proceed despite already being assigned) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + // Get global PR repository (default-pr-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "default-pr-repo-id", default_branch: "main" } }); + // Get per-item PR repository (other-platform-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "other-platform-repo-id", default_branch: "main" } }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details - agent is already assigned + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); + // Assign agent (re-assignment allowed) + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -541,10 +432,7 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned copilot coding agent to issue #42")); expect(mockCore.setFailed).not.toHaveBeenCalled(); - // Verify the mutation was called with the per-item PR repo ID - const lastGraphQLCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; - expect(lastGraphQLCall[0]).toContain("agentAssignment"); - expect(lastGraphQLCall[1].targetRepoId).toBe("other-platform-repo-id"); + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: "test-owner", repo: "other-platform-repo" })); }); it("should process multiple assignments for the same temporary issue ID across different pull_request_repo targets", async () => { @@ -572,71 +460,34 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Item 1: get per-item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "ios-repo-id", - }, - }) - // Item 1: find agent - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], - }, - }, - }) - // Item 1: issue details (not assigned yet) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [], - }, - }, - }, - }) - // Item 1: assignment mutation - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - // Item 2: get per-item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "android-repo-id", - }, - }) - // Item 2: issue details (already assigned after item 1) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], - }, - }, - }, - }) - // Item 2: assignment mutation should still run - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Item 1: per-item PR repository (ios-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "ios-repo-id", default_branch: "main" } }); + // Item 1: find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Item 1: issue details (not assigned yet) + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 6587, assignees: [], html_url: "", title: "", body: "" }, + }); + // Item 1: assignment + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); + // Item 2: per-item PR repository (android-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "android-repo-id", default_branch: "main" } }); + // Item 2: issue details (already assigned, but re-assignment allowed for different repo) + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 6587, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); + // Item 2: assignment (re-assignment allowed) + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-2" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #6587")); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned copilot coding agent to issue #6587")); - const assignmentCalls = mockGithub.graphql.mock.calls.filter(([query]) => query.includes("replaceActorsForAssignable")); - expect(assignmentCalls).toHaveLength(2); - expect(assignmentCalls[0][1].targetRepoId).toBe("ios-repo-id"); - expect(assignmentCalls[1][1].targetRepoId).toBe("android-repo-id"); + expect(mockGithub.request).toHaveBeenCalledTimes(2); + expect(mockGithub.request.mock.calls[0][1]).toMatchObject({ owner: "test-owner", repo: "ios-repo" }); + expect(mockGithub.request.mock.calls[1][1]).toMatchObject({ owner: "test-owner", repo: "android-repo" }); expect(mockSleep).toHaveBeenCalledTimes(1); expect(mockSleep).toHaveBeenCalledWith(10000); @@ -670,61 +521,28 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Item 1: get per-item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "ios-repo-id", - }, - }) - // Item 1: find agent - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], - }, - }, - }) - // Item 1: issue details (not assigned yet) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [], - }, - }, - }, - }) - // Item 1: assignment mutation - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - // Item 2: get per-item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "ios-repo-id", - }, - }) - // Item 2: issue details (already assigned after item 1) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], - }, - }, - }, - }); + // Item 1: per-item PR repository (ios-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "ios-repo-id", default_branch: "main" } }); + // Item 1: find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Item 1: issue details (not assigned yet) + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 6587, assignees: [], html_url: "", title: "", body: "" }, + }); + // Item 1: assignment + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); + // Item 2: per-item PR repository (ios-repo, same) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "ios-repo-id", default_branch: "main" } }); + // Item 2: issue details (already assigned, same context → skip) + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 6587, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #6587")); - const assignmentCalls = mockGithub.graphql.mock.calls.filter(([query]) => query.includes("replaceActorsForAssignable")); - expect(assignmentCalls).toHaveLength(1); + expect(mockGithub.request).toHaveBeenCalledTimes(1); expect(mockSleep).toHaveBeenCalledTimes(1); expect(mockSleep).toHaveBeenCalledWith(10000); }); @@ -742,32 +560,18 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Find agent - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], - }, - }, - }) - // Get issue details - already assigned - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], - }, - }, - }, - }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details - already assigned + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); - const assignmentCalls = mockGithub.graphql.mock.calls.filter(([query]) => query.includes("replaceActorsForAssignable")); - expect(assignmentCalls).toHaveLength(0); + expect(mockGithub.request).not.toHaveBeenCalled(); }); it("should still skip when agent is already assigned with global pull-request-repo but no per-item override", async () => { @@ -783,41 +587,24 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - // Get global PR repository ID and default branch - .mockResolvedValueOnce({ - repository: { - id: "global-pr-repo-id", - defaultBranchRef: { name: "main" }, - }, - }) - // Find agent - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "agent-id" }], - }, - }, - }) - // Get issue details - agent is already assigned - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [{ id: "agent-id", login: "copilot-swe-agent" }], - }, - }, - }, - }); + // Mock REST responses + // Get global PR repository ID and default branch + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "global-pr-repo-id", default_branch: "main" } }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details - agent is already assigned + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [{ id: 99999, login: "copilot-swe-agent" }], html_url: "", title: "", body: "" }, + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should see "already assigned" skip message expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); // Should NOT have called the assignment mutation (only 3 GraphQL calls: repo lookup, find agent, get issue) - expect(mockGithub.graphql).toHaveBeenCalledTimes(3); + expect(mockGithub.rest.repos.get).toHaveBeenCalledTimes(1); // global PR repo lookup + expect(mockGithub.request).not.toHaveBeenCalled(); // no assignment since already assigned expect(mockCore.setFailed).not.toHaveBeenCalled(); }); @@ -834,7 +621,7 @@ describe("assign_to_agent", () => { }); const apiError = new Error("API rate limit exceeded"); - mockGithub.graphql.mockRejectedValue(apiError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(apiError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -855,30 +642,20 @@ describe("assign_to_agent", () => { }); // Mock successful agent lookup and issue details - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { nodes: [] }, - }, - }, - }) - .mockRejectedValueOnce({ - response: { - status: 502, - url: "https://api.github.com/graphql", - headers: { "content-type": "text/html" }, - data: "\n502 Bad Gateway\n\n

502 Bad Gateway

\n
nginx
\n\n\n", - }, - }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + // Assignment fails with 502 + mockGithub.request.mockRejectedValueOnce({ + response: { + status: 502, + url: "https://api.github.com/repos/test-owner/test-repo/tasks", + headers: { "content-type": "text/html" }, + data: "\n502 Bad Gateway\n\n

502 Bad Gateway

\n
nginx
\n\n\n", + }, + }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -904,23 +681,13 @@ describe("assign_to_agent", () => { }); // Mock successful agent lookup and issue details - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { nodes: [] }, - }, - }, - }) - .mockRejectedValueOnce(new Error("502 Bad Gateway")); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + // Assignment fails with 502 message + mockGithub.request.mockRejectedValueOnce(new Error("502 Bad Gateway")); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -939,41 +706,22 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-1", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-2", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 11111, number: 1, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 22222, number: 2, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-2" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should only look up agent once (cached for second assignment) - const graphqlCalls = mockGithub.graphql.mock.calls.filter(call => call[0].includes("suggestedActors")); - expect(graphqlCalls).toHaveLength(1); + expect(mockGithub.rest.issues.checkUserCanBeAssigned).toHaveBeenCalledTimes(1); }, 15000); // Increase timeout to 15 seconds to account for the delay it("should use target repository when configured", async () => { @@ -990,15 +738,6 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }); - await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith("Default target repo: other-owner/other-repo"); @@ -1036,30 +775,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Simulate permission error during agent assignment mutation (not during getIssueDetails) - // First call: findAgent succeeds - // Second call: getIssueDetails succeeds - // Third call: assignAgentToIssue fails with permission error const permissionError = new Error("Resource not accessible by integration"); - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id", - assignees: { - nodes: [], - }, - }, - }, - }) - .mockRejectedValueOnce(permissionError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockRejectedValueOnce(permissionError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1085,30 +807,12 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses for PR - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - pullRequest: { - id: "pr-id-123", - assignees: { - nodes: [], - }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.pulls.get.mockResolvedValueOnce({ + data: { id: 67890, number: 123, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-pr-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1161,30 +865,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses in the correct order - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "issue-id-123", - assignees: { - nodes: [], - }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses in the correct order + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 123, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1194,7 +881,7 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Target configuration: triggering")); // GraphQL should have been called for finding the agent and getting issue details - expect(mockGithub.graphql).toHaveBeenCalled(); + expect(mockGithub.request).toHaveBeenCalled(); }); it("should skip when context doesn't match triggering target", async () => { @@ -1254,25 +941,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=", __typename: "Bot" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1305,7 +980,7 @@ describe("assign_to_agent", () => { expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to assign 1 agent(s)")); // Should not have made any GraphQL calls since validation failed early - expect(mockGithub.graphql).not.toHaveBeenCalled(); + expect(mockGithub.request).not.toHaveBeenCalled(); }); it("should allow any agent when no allowed list is configured", async () => { @@ -1321,25 +996,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1364,7 +1027,7 @@ describe("assign_to_agent", () => { // Simulate authentication error - use mockRejectedValueOnce to avoid affecting other tests const authError = new Error("Bad credentials"); - mockGithub.graphql.mockRejectedValueOnce(authError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValueOnce(authError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1400,7 +1063,7 @@ describe("assign_to_agent", () => { // Simulate authentication error const authError = new Error("Bad credentials"); - mockGithub.graphql.mockRejectedValue(authError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(authError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1437,7 +1100,7 @@ describe("assign_to_agent", () => { // Simulate permission error const permError = new Error("Resource not accessible by integration"); - mockGithub.graphql.mockRejectedValue(permError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(permError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1461,7 +1124,7 @@ describe("assign_to_agent", () => { // Simulate a different error (not auth-related) const otherError = new Error("Network timeout"); - mockGithub.graphql.mockRejectedValue(otherError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(otherError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1482,25 +1145,12 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "I_abc123", - assignees: { nodes: [] }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" }, - }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1516,7 +1166,7 @@ describe("assign_to_agent", () => { // Fail all assignments with auth error const authError = new Error("Bad credentials"); - mockGithub.graphql.mockRejectedValue(authError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(authError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1541,7 +1191,7 @@ describe("assign_to_agent", () => { // Simulate an error whose message contains an @mention and an HTML comment — // both are potentially dangerous if posted unsanitized. const dangerousError = new Error("@admin triggered error"); - mockGithub.graphql.mockRejectedValue(dangerousError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(dangerousError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1570,7 +1220,7 @@ describe("assign_to_agent", () => { // Simulate authentication error (will be skipped by ignore-if-error) const authError = new Error("Bad credentials"); - mockGithub.graphql.mockRejectedValue(authError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(authError); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1591,7 +1241,7 @@ describe("assign_to_agent", () => { }); const authError = new Error("Bad credentials"); - mockGithub.graphql.mockRejectedValue(authError); + mockGithub.rest.issues.checkUserCanBeAssigned.mockRejectedValue(authError); // Simulate failure to post comment mockGithub.rest.issues.createComment.mockRejectedValue(new Error("Could not post comment")); @@ -1619,45 +1269,14 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses for all three assignments - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-1", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-2", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id-3", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 11111, number: 1, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-1" } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 22222, number: 2, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-2" } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 33333, number: 3, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-3" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1705,25 +1324,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1746,25 +1353,13 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { id: "issue-id", assignees: { nodes: [] } }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { - __typename: "ReplaceActorsForAssignablePayload", - }, - }); + // Mock REST responses + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1789,49 +1384,23 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - // Get PR repository ID and default branch - .mockResolvedValueOnce({ - repository: { - id: "pull-request-repo-id", - defaultBranchRef: { name: "main" }, - }, - }) - // 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", - }, - }); + // Mock REST responses + // Get PR repository (pull-request-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "pull-request-repo-id", default_branch: "main" } }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + // Assign agent + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); 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("pull-request-repo-id"); + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: "test-owner", repo: "pull-request-repo" })); }); it("should handle per-item pull_request_repo parameter", async () => { @@ -1851,52 +1420,26 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - // Get global PR repository ID and default branch (for default-pr-repo) - .mockResolvedValueOnce({ - repository: { - id: "default-pr-repo-id", - defaultBranchRef: { name: "main" }, - }, - }) - // Get item PR repository ID - .mockResolvedValueOnce({ - repository: { - id: "item-pull-request-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", - }, - }); + // Mock REST responses + // Get global PR repository ID and default branch (for default-pr-repo) + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "default-pr-repo-id", default_branch: "main" } }); + // Get item PR repository + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "item-pull-request-repo-id", default_branch: "main" } }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + // Assign agent + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); 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-pull-request-repo-id"); + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: "test-owner", repo: "item-pull-request-repo" })); }); it("should reject per-item pull_request_repo not in allowed list", async () => { @@ -1915,12 +1458,7 @@ describe("assign_to_agent", () => { }); // Mock global PR repo lookup - mockGithub.graphql.mockResolvedValueOnce({ - repository: { - id: "default-pr-repo-id", - defaultBranchRef: { name: "main" }, - }, - }); + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "default-pr-repo-id", default_branch: "main" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1943,38 +1481,18 @@ describe("assign_to_agent", () => { errors: [], }); - // Mock GraphQL responses - mockGithub.graphql - // Get PR repository ID and default branch - .mockResolvedValueOnce({ - repository: { - id: "auto-allowed-repo-id", - defaultBranchRef: { name: "main" }, - }, - }) - // 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", - }, - }); + // Mock REST responses + // Get PR repository ID and default branch + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "auto-allowed-repo-id", default_branch: "main" } }); + // Find agent + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + // Get issue details + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + // Assign agent + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); @@ -1991,25 +1509,19 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Get PR repo ID and default branch - .mockResolvedValueOnce({ repository: { id: "code-repo-id", defaultBranchRef: { name: "main" } } }) - // 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 - .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "code-repo-id", default_branch: "main" } }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); - // Verify the mutation was called with baseRef set to the explicit base-branch - const lastCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; - expect(lastCall[0]).toContain("baseRef: $baseRef"); - expect(lastCall[1].baseRef).toBe("develop"); - // customInstructions should NOT contain the branch instruction text - expect(lastCall[1].customInstructions).toBeUndefined(); + // Verify the request was called with base_ref set to the explicit base-branch + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ base_ref: "develop" })); + const requestArgs = mockGithub.request.mock.calls[0][1]; + expect(requestArgs.customInstructions).toBeUndefined(); }); it("should auto-resolve non-main default branch from pull-request-repo and set as baseRef", async () => { @@ -2020,24 +1532,18 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Get PR repo ID and default branch (non-main) - .mockResolvedValueOnce({ repository: { id: "code-repo-id", defaultBranchRef: { name: "develop" } } }) - // 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 - .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "code-repo-id", default_branch: "develop" } }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved pull request repository default branch: develop")); - // Verify the mutation was called with baseRef set to the resolved default branch - const lastCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; - expect(lastCall[0]).toContain("baseRef: $baseRef"); - expect(lastCall[1].baseRef).toBe("develop"); + // Verify the request was called with base_ref set to the resolved default branch + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ base_ref: "develop" })); }); it("should set baseRef when pull-request-repo default branch is main (no explicit base-branch)", async () => { @@ -2048,23 +1554,18 @@ describe("assign_to_agent", () => { errors: [], }); - mockGithub.graphql - // Get PR repo ID and default branch (main) - .mockResolvedValueOnce({ repository: { id: "code-repo-id", defaultBranchRef: { name: "main" } } }) - // 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 - .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); + mockGithub.rest.repos.get.mockResolvedValueOnce({ data: { node_id: "code-repo-id", default_branch: "main" } }); + mockGithub.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + mockGithub.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" } }); + mockGithub.request.mockResolvedValueOnce({ data: { id: "task-123" } }); await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); - // Verify the mutation was called with baseRef set to the repo's default branch - const lastCall = mockGithub.graphql.mock.calls[mockGithub.graphql.mock.calls.length - 1]; - expect(lastCall[0]).toContain("baseRef: $baseRef"); - expect(lastCall[1].baseRef).toBe("main"); - expect(lastCall[1].customInstructions).toBeUndefined(); + // Verify the request was called with base_ref set to the repo's default branch + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ base_ref: "main" })); + const requestArgs = mockGithub.request.mock.calls[0][1]; + expect(requestArgs.customInstructions).toBeUndefined(); }); }); diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index e69f46fd907..65f26ee99fc 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3151,6 +3151,7 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { // Push fails to trigger the fallback-issue path; issue creation succeeds global.github = { + request: vi.fn().mockResolvedValue({ data: { id: "task-123" } }), rest: { pulls: { create: vi.fn().mockRejectedValue(Object.assign(new Error("Permission denied"), { status: 403 })), @@ -3162,9 +3163,13 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { issues: { create: vi.fn().mockResolvedValue({ data: { number: 99, html_url: "https://github.com/test/issues/99" } }), addLabels: vi.fn().mockResolvedValue({}), + checkUserCanBeAssigned: vi.fn().mockResolvedValue({}), + get: vi.fn().mockResolvedValue({ data: { id: 12345, number: 99, assignees: [], html_url: "", title: "", body: "" } }), + }, + users: { + getByUsername: vi.fn().mockResolvedValue({ data: { id: 99999 } }), }, }, - graphql: vi.fn(), }; global.context = { @@ -3208,50 +3213,37 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { vi.clearAllMocks(); }); - it("should not call graphql for copilot assignment when GH_AW_ASSIGN_COPILOT is not set", async () => { + it("should not call request for copilot assignment when GH_AW_ASSIGN_COPILOT is not set", async () => { delete process.env.GH_AW_ASSIGN_COPILOT; const { main } = require("./create_pull_request.cjs"); const handler = await main({ assignees: ["copilot"], allow_empty: true }); await handler({ title: "Test PR", body: "Test body" }, {}); - // No graphql calls for copilot assignment - expect(global.github.graphql).not.toHaveBeenCalled(); + // No REST task creation calls for copilot assignment + expect(global.github.request).not.toHaveBeenCalled(); }); - it("should not call graphql when copilot is not in assignees even if GH_AW_ASSIGN_COPILOT is true", async () => { + it("should not call request when copilot is not in assignees even if GH_AW_ASSIGN_COPILOT is true", async () => { process.env.GH_AW_ASSIGN_COPILOT = "true"; const { main } = require("./create_pull_request.cjs"); const handler = await main({ assignees: ["user1"], allow_empty: true }); await handler({ title: "Test PR", body: "Test body" }, {}); - expect(global.github.graphql).not.toHaveBeenCalled(); + expect(global.github.request).not.toHaveBeenCalled(); }); - it("should strip copilot from REST assignees for fallback issue but assign via graphql when enabled", async () => { + it("should strip copilot from REST assignees for fallback issue but assign via REST task when enabled", async () => { process.env.GH_AW_ASSIGN_COPILOT = "true"; // Mock findAgent → getIssueDetails → assignAgentToIssue - global.github.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ id: "COPILOT_AGENT_ID", login: "copilot-swe-agent", __typename: "Bot" }], - }, - }, - }) - .mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_NODE_ID", - assignees: { nodes: [] }, - }, - }, - }) - .mockResolvedValueOnce({ - replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" }, - }); + global.github.rest.issues.checkUserCanBeAssigned.mockResolvedValueOnce({}); + global.github.rest.users.getByUsername.mockResolvedValueOnce({ data: { id: 99999 } }); + global.github.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 99, assignees: [], html_url: "", title: "", body: "" }, + }); + global.github.request.mockResolvedValueOnce({ data: { id: "task-123" } }); const { main } = require("./create_pull_request.cjs"); const handler = await main({ assignees: ["copilot", "user1"], allow_empty: true }); @@ -3262,8 +3254,8 @@ describe("create_pull_request - copilot assignee on fallback issues", () => { expect(issueCall.assignees).not.toContain("copilot"); expect(issueCall.assignees).toContain("user1"); - // Graphql should be called for copilot assignment - expect(global.github.graphql).toHaveBeenCalledTimes(3); + // REST task creation should be called once for copilot assignment + expect(global.github.request).toHaveBeenCalledTimes(1); }); it("should use configured fallback_labels for fallback issues instead of PR labels", async () => { From 26ff1e48904a1dac53d32dc7971770f0f2c9075e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:41:56 +0000 Subject: [PATCH 8/9] fix: correct assignAgentToIssue call site args and update test to REST mocks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_issue.cjs | 2 +- actions/setup/js/create_issue.test.cjs | 44 ++++++++++++-------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index fa4c3f20058..ac6626f4702 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -1083,7 +1083,7 @@ async function main(config = {}) { } else if (issueDetails.currentAssignees.some(a => a.id === agentId)) { core.info(`copilot is already assigned to issue #${issue.number}`); } else { - const assigned = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, "copilot", null, null, null, null, null, null, copilotClient); + const assigned = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, "copilot", null, null, null, null, null, copilotClient, issueDetails.taskContext); if (assigned) { core.info(`Successfully assigned copilot coding agent to issue #${issue.number}`); } else { diff --git a/actions/setup/js/create_issue.test.cjs b/actions/setup/js/create_issue.test.cjs index f4519a7cb8f..e46ebece1d3 100644 --- a/actions/setup/js/create_issue.test.cjs +++ b/actions/setup/js/create_issue.test.cjs @@ -370,36 +370,32 @@ describe("create_issue", () => { it("should assign copilot directly when enabled", async () => { process.env.GH_AW_ASSIGN_COPILOT = "true"; - // Mock findAgent - mockGithub.graphql - .mockResolvedValueOnce({ - repository: { - suggestedActors: { - nodes: [{ id: "COPILOT_AGENT_ID", login: "copilot-swe-agent", __typename: "Bot" }], - }, - }, - }) - // Mock getIssueDetails - .mockResolvedValueOnce({ - repository: { - issue: { - id: "ISSUE_NODE_ID", - assignees: { nodes: [] }, - }, - }, - }) - // Mock assignAgentToIssue mutation - .mockResolvedValueOnce({ - replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" }, - }); + // Mock findAgent (REST: checkUserCanBeAssigned + users.getByUsername) + mockGithub.rest.issues.checkUserCanBeAssigned = vi.fn().mockResolvedValue({}); + mockGithub.rest.users = { getByUsername: vi.fn().mockResolvedValue({ data: { id: 42, login: "copilot-swe-agent" } }) }; + + // Mock getIssueDetails (REST: issues.get) + mockGithub.rest.issues.get = vi.fn().mockResolvedValue({ + data: { + id: 456, + number: 123, + html_url: "https://github.com/test-owner/test-repo/issues/123", + title: "Test Issue", + body: "", + assignees: [], + }, + }); + + // Mock assignAgentToIssue (REST: request) + mockGithub.request = vi.fn().mockResolvedValue({ data: { id: "task-123" } }); const handler = await main({ assignees: ["copilot"], }); await handler({ title: "Test" }); - // Verify graphql was called three times (findAgent, getIssueDetails, assignAgentToIssue) - expect(mockGithub.graphql).toHaveBeenCalledTimes(3); + // Verify REST task creation was called + expect(mockGithub.request).toHaveBeenCalledWith("POST /agents/repos/{owner}/{repo}/tasks", expect.objectContaining({ owner: expect.any(String), repo: expect.any(String) })); }); }); From 0af27e31b3843ee68f877f6de918687a5609d658 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:53:08 +0000 Subject: [PATCH 9/9] fix: add repos.get mock to check_workflow_recompile_needed tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_workflow_recompile_needed.test.cjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/actions/setup/js/check_workflow_recompile_needed.test.cjs b/actions/setup/js/check_workflow_recompile_needed.test.cjs index 68f75570d7d..0405b1c8c32 100644 --- a/actions/setup/js/check_workflow_recompile_needed.test.cjs +++ b/actions/setup/js/check_workflow_recompile_needed.test.cjs @@ -139,6 +139,14 @@ The following workflow lock files have changes: git: { createRef: vi.fn(), }, + repos: { + get: vi.fn().mockResolvedValue({ + data: { + node_id: "R_testowner_testrepo", + default_branch: "main", + }, + }), + }, }, graphql: vi.fn().mockImplementation(query => { if (String(query).includes("createCommitOnBranch")) {