diff --git a/.github/workflows/ai-triage-campaign.lock.yml b/.github/workflows/ai-triage-campaign.lock.yml index f3fa15022be..a2890423ff6 100644 --- a/.github/workflows/ai-triage-campaign.lock.yml +++ b/.github/workflows/ai-triage-campaign.lock.yml @@ -25,13 +25,17 @@ # graph LR # activation["activation"] # agent["agent"] +# assign_to_agent["assign_to_agent"] # conclusion["conclusion"] # detection["detection"] # missing_tool["missing_tool"] # update_project["update_project"] # activation --> agent +# agent --> assign_to_agent +# detection --> assign_to_agent # agent --> conclusion # activation --> conclusion +# assign_to_agent --> conclusion # missing_tool --> conclusion # update_project --> conclusion # agent --> detection @@ -48,8 +52,9 @@ # ## Your Mission # # 1. **Fetch open issues** - Query for open issues in this repository (max ${{ github.event.inputs.max_issues }} most recent, default: 10) -# 2. **Analyze each issue** - Determine if it's well-suited for AI agent resolution -# 3. **Route to project board** - Add each issue to project ${{ github.event.inputs.project_url }} with intelligent field assignments +# 2. **Filter unassigned issues** - Skip issues that already have assignees (do NOT add them to the project board) +# 3. **Analyze unassigned issues** - Determine if each unassigned issue is well-suited for AI agent resolution +# 4. **Route to project board** - Add only unassigned issues to project ${{ github.event.inputs.project_url }} with intelligent field assignments # # ## AI Agent Suitability Assessment # @@ -209,11 +214,21 @@ # ## Assignment Strategy # # **Immediately assign @copilot when:** +# - Issue is currently **unassigned** (no existing assignees) # - AI-Readiness Score ≥ 9 # - Issue has clear acceptance criteria # - All context is provided # - No external dependencies # +# **Action:** Output an `assign_to_agent` safe-output item for these high-readiness issues: +# ```json +# { +# "type": "assign_to_agent", +# "issue_number": 123, +# "agent": "copilot" +# } +# ``` +# # **For lower scores (5-8):** # - Route to "AI Agent Potential" board # - Don't assign yet - needs clarification first @@ -248,7 +263,7 @@ # - Priority: [priority + brief reason] # # 3. **Assignment Decision** -# - If score ≥ 9: "Assigning to @copilot for immediate work" +# - If score ≥ 9: "Assigning to @copilot for immediate work" + output assign_to_agent # - If score 5-8: "Needs [specific clarifications] before assignment" # - If score < 5: "Requires human review - [specific reasons]" # @@ -263,14 +278,17 @@ # ## Workflow Steps # # 1. **Fetch Issues**: Use GitHub MCP to query up to ${{ github.event.inputs.max_issues }} most recent open issues (default: 10) -# 2. **Score Each Issue**: Evaluate AI-readiness based on the criteria above -# 3. **Route to Project Board**: For each issue, output an `update_project` safe-output item with `"project": "${{ github.event.inputs.project_url }}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments +# 2. **Filter Unassigned**: Skip any issues that already have assignees (human or agent) - do NOT process or add them to the project board +# 3. **Score Each Issue**: Evaluate AI-readiness based on the criteria above (only for unassigned issues) +# 4. **Route to Project Board**: For each unassigned issue, output an `update_project` safe-output item with `"project": "${{ github.event.inputs.project_url }}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments +# 5. **Assign High-Readiness Issues**: For unassigned issues with AI-Readiness Score ≥ 9, output an `assign_to_agent` safe-output item to immediately assign the issue to @copilot # # ## Execution Notes # # - This workflow runs every 4 hours automatically (or manually with custom parameters) # - Input defaults: max_issues=10, project_url=https://github.com/orgs/githubnext/projects/53 -# - All issues are routed to the project board with differentiation via Status field +# - **Only unassigned issues** are routed to the project board (issues with existing assignees are completely skipped) +# - All unassigned issues are routed to the project board with differentiation via Status field # - Custom fields are created automatically if they don't exist # - User projects must exist before workflow runs (cannot auto-create) # ``` @@ -301,8 +319,10 @@ name: "AI Triage Campaign" required: false permissions: - contents: read - issues: read + actions: write + contents: write + issues: write + pull-requests: write repository-projects: write concurrency: @@ -411,8 +431,10 @@ jobs: needs: activation runs-on: ubuntu-latest permissions: - contents: read - issues: read + actions: write + contents: write + issues: write + pull-requests: write repository-projects: write env: GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl @@ -509,10 +531,10 @@ jobs: run: | mkdir -p /tmp/gh-aw/safeoutputs cat > /tmp/gh-aw/safeoutputs/config.json << 'EOF' - {"missing_tool":{"max":0},"noop":{"max":1},"update_project":{"max":20}} + {"assign_to_agent":{"default_agent":"copilot","max":10},"missing_tool":{"max":0},"noop":{"max":1},"update_project":{"max":20}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] + [{"description":"Assign a GitHub Copilot agent to an issue or project item. The agent will be notified and can start working on the issue.","inputSchema":{"additionalProperties":false,"properties":{"agent":{"description":"Agent name or slug (defaults to 'copilot' if not provided)","type":"string"},"issue_number":{"description":"Issue number to assign agent to","type":["number","string"]}},"required":["issue_number"],"type":"object"},"name":"assign_to_agent"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1256,8 +1278,9 @@ jobs: ## Your Mission 1. **Fetch open issues** - Query for open issues in this repository (max ${GH_AW_EXPR_0D0C2AD6} most recent, default: 10) - 2. **Analyze each issue** - Determine if it's well-suited for AI agent resolution - 3. **Route to project board** - Add each issue to project ${GH_AW_EXPR_7B9A5317} with intelligent field assignments + 2. **Filter unassigned issues** - Skip issues that already have assignees (do NOT add them to the project board) + 3. **Analyze unassigned issues** - Determine if each unassigned issue is well-suited for AI agent resolution + 4. **Route to project board** - Add only unassigned issues to project ${GH_AW_EXPR_7B9A5317} with intelligent field assignments ## AI Agent Suitability Assessment @@ -1417,11 +1440,21 @@ jobs: ## Assignment Strategy **Immediately assign @copilot when:** + - Issue is currently **unassigned** (no existing assignees) - AI-Readiness Score ≥ 9 - Issue has clear acceptance criteria - All context is provided - No external dependencies + **Action:** Output an `assign_to_agent` safe-output item for these high-readiness issues: + ```json + { + "type": "assign_to_agent", + "issue_number": 123, + "agent": "copilot" + } + ``` + **For lower scores (5-8):** - Route to "AI Agent Potential" board - Don't assign yet - needs clarification first @@ -1456,7 +1489,7 @@ jobs: - Priority: [priority + brief reason] 3. **Assignment Decision** - - If score ≥ 9: "Assigning to @copilot for immediate work" + - If score ≥ 9: "Assigning to @copilot for immediate work" + output assign_to_agent - If score 5-8: "Needs [specific clarifications] before assignment" - If score < 5: "Requires human review - [specific reasons]" @@ -1471,14 +1504,17 @@ jobs: ## Workflow Steps 1. **Fetch Issues**: Use GitHub MCP to query up to ${GH_AW_EXPR_0D0C2AD6} most recent open issues (default: 10) - 2. **Score Each Issue**: Evaluate AI-readiness based on the criteria above - 3. **Route to Project Board**: For each issue, output an `update_project` safe-output item with `"project": "${GH_AW_EXPR_7B9A5317}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments + 2. **Filter Unassigned**: Skip any issues that already have assignees (human or agent) - do NOT process or add them to the project board + 3. **Score Each Issue**: Evaluate AI-readiness based on the criteria above (only for unassigned issues) + 4. **Route to Project Board**: For each unassigned issue, output an `update_project` safe-output item with `"project": "${GH_AW_EXPR_7B9A5317}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments + 5. **Assign High-Readiness Issues**: For unassigned issues with AI-Readiness Score ≥ 9, output an `assign_to_agent` safe-output item to immediately assign the issue to @copilot ## Execution Notes - This workflow runs every 4 hours automatically (or manually with custom parameters) - Input defaults: max_issues=10, project_url=https://github.com/orgs/githubnext/projects/53 - - All issues are routed to the project board with differentiation via Status field + - **Only unassigned issues** are routed to the project board (issues with existing assignees are completely skipped) + - All unassigned issues are routed to the project board with differentiation via Status field - Custom fields are created automatically if they don't exist - User projects must exist before workflow runs (cannot auto-create) @@ -1539,10 +1575,14 @@ jobs: --- - ## Reporting Missing Tools or Functionality + ## Assigning Agents to Issues, Reporting Missing Tools or Functionality **IMPORTANT**: To do the actions mentioned in the header of this section, use the **safeoutputs** tools, do NOT attempt to use `gh`, do NOT attempt to use the GitHub API. You don't have write access to the GitHub repo. + **Assigning Agents to Issues** + + To assign a GitHub Copilot agent to an issue, use the assign-to-agent tool from safeoutputs + **Reporting Missing Tools or Functionality** To report a missing tool use the missing-tool tool from safeoutputs. @@ -4141,10 +4181,495 @@ jobs: main(); } + assign_to_agent: + needs: + - agent + - detection + if: > + (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent'))) && + (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + actions: write + contents: write + issues: write + pull-requests: write + timeout-minutes: 10 + outputs: + assigned_agents: ${{ steps.assign_to_agent.outputs.assigned_agents }} + steps: + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: agent_output.json + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Assign to Agent + id: assign_to_agent + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_AGENT_DEFAULT: "copilot" + GH_AW_AGENT_MAX_COUNT: 10 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GH_AW_WORKFLOW_NAME: "AI Triage Campaign" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.error(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function generateStagedPreview(options) { + const { title, description, items, renderItem } = options; + let summaryContent = `## 🎭 Staged Mode: ${title} Preview\n\n`; + summaryContent += `${description}\n\n`; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + summaryContent += renderItem(item, i); + summaryContent += "---\n\n"; + } + try { + await core.summary.addRaw(summaryContent).write(); + core.info(summaryContent); + core.info(`📝 ${title} preview written to step summary`); + } catch (error) { + core.setFailed(error instanceof Error ? error : String(error)); + } + } + const AGENT_LOGIN_NAMES = { + copilot: "copilot-swe-agent", + claude: "claude-swe-agent", + codex: "codex-swe-agent", + }; + async function getAvailableAgentLogins(owner, repo) { + const query = ` + query { + repository(owner: "${owner}", name: "${repo}") { + suggestedActors(first: 100, capabilities: CAN_BE_ASSIGNED) { + nodes { ... on Bot { login __typename } } + } + } + } + `; + try { + const response = await github.graphql(query); + const actors = response.repository?.suggestedActors?.nodes || []; + const knownValues = Object.values(AGENT_LOGIN_NAMES); + const available = []; + for (const actor of actors) { + if (actor && actor.login && knownValues.includes(actor.login)) { + available.push(actor.login); + } + } + return available.sort(); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + core.debug(`Failed to list available agent logins: ${msg}`); + return []; + } + } + async function findAgent(owner, repo, agentName) { + const query = ` + query { + repository(owner: "${owner}", name: "${repo}") { + suggestedActors(first: 100, capabilities: CAN_BE_ASSIGNED) { + nodes { + ... on Bot { + id + login + __typename + } + } + } + } + } + `; + try { + const response = await github.graphql(query); + 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(", ")}`); + return null; + } + for (const actor of actors) { + if (actor.login === loginName) { + return actor.id; + } + } + const available = actors + .filter(a => a && a.login && Object.values(AGENT_LOGIN_NAMES).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 = error instanceof Error ? error.message : String(error); + core.error(`Failed to find ${agentName} agent: ${errorMessage}`); + return null; + } + } + async function getIssueDetails(owner, repo, issueNumber) { + const query = ` + query { + repository(owner: "${owner}", name: "${repo}") { + issue(number: ${issueNumber}) { + id + assignees(first: 100) { + nodes { + id + } + } + } + } + } + `; + try { + const response = await github.graphql(query); + 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 => assignee.id); + return { + issueId: issue.id, + currentAssignees: currentAssignees, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.error(`Failed to get issue details: ${errorMessage}`); + return null; + } + } + async function assignAgentToIssue(issueId, agentId, currentAssignees, agentName) { + const actorIds = [agentId]; + for (const assigneeId of currentAssignees) { + if (assigneeId !== agentId) { + actorIds.push(assigneeId); + } + } + const mutation = ` + mutation { + replaceActorsForAssignable(input: { + assignableId: "${issueId}", + actorIds: ${JSON.stringify(actorIds)} + }) { + __typename + } + } + `; + try { + const response = await github.graphql(mutation); + if (response.replaceActorsForAssignable && response.replaceActorsForAssignable.__typename) { + return true; + } else { + core.error("Unexpected response from GitHub API"); + return false; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + try { + core.debug(`Raw GraphQL error message: ${errorMessage}`); + if (error && typeof error === "object") { + const details = {}; + if (error.errors) details.errors = error.errors; + if (error.response) details.response = error.response; + if (error.data) details.data = error.data; + if (Array.isArray(error.errors)) { + details.compactMessages = error.errors.map(e => e.message).filter(Boolean); + } + const serialized = JSON.stringify(details, (_k, v) => v, 2); + if (serialized && serialized !== '{}' ) { + core.debug(`Raw GraphQL error details: ${serialized}`); + core.error("Raw GraphQL error details (for troubleshooting):"); + for (const line of serialized.split(/\n/)) { + if (line.trim()) core.error(line); + } + } + } + } catch (loggingErr) { + core.debug(`Failed to serialize GraphQL error details: ${loggingErr instanceof Error ? loggingErr.message : String(loggingErr)}`); + } + if ( + errorMessage.includes("Resource not accessible by personal access token") || + errorMessage.includes("Resource not accessible by integration") || + errorMessage.includes("Insufficient permissions to assign") + ) { + core.info("Primary mutation replaceActorsForAssignable forbidden. Attempting fallback addAssigneesToAssignable..."); + try { + const fallbackMutation = `mutation {\n addAssigneesToAssignable(input:{assignableId:"${issueId}", assigneeIds:["${agentId}"]}) {\n clientMutationId\n }\n}`; + const fallbackResp = await github.graphql(fallbackMutation); + if (fallbackResp && fallbackResp.addAssigneesToAssignable) { + core.info(`Fallback succeeded: agent '${agentName}' added via addAssigneesToAssignable.`); + return true; + } else { + core.warning("Fallback mutation returned unexpected response; proceeding with permission guidance."); + } + } catch (fallbackError) { + const fbMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); + core.error(`Fallback addAssigneesToAssignable failed: ${fbMsg}`); + } + core.error(`Failed to assign ${agentName}: Insufficient permissions`); + core.error(""); + core.error("Assigning Copilot agents requires:"); + core.error(" 1. All four workflow permissions:"); + core.error(" - actions: write"); + core.error(" - contents: write"); + core.error(" - issues: write"); + core.error(" - pull-requests: 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(""); + 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(" - 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"); + } else { + core.error(`Failed to assign ${agentName}: ${errorMessage}`); + } + return false; + } + } + async function main() { + const result = loadAgentOutput(); + if (!result.success) { + return; + } + const assignItems = result.items.filter(item => item.type === "assign_to_agent"); + if (assignItems.length === 0) { + core.info("No assign_to_agent items found in agent output"); + return; + } + core.info(`Found ${assignItems.length} assign_to_agent item(s)`); + if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { + await generateStagedPreview({ + title: "Assign to Agent", + description: "The following agent assignments would be made if staged mode was disabled:", + items: assignItems, + renderItem: item => { + let content = `**Issue:** #${item.issue_number}\n`; + content += `**Agent:** ${item.agent || "copilot"}\n`; + content += "\n"; + return content; + }, + }); + return; + } + const defaultAgent = process.env.GH_AW_AGENT_DEFAULT?.trim() || "copilot"; + core.info(`Default agent: ${defaultAgent}`); + const maxCountEnv = process.env.GH_AW_AGENT_MAX_COUNT; + const maxCount = maxCountEnv ? parseInt(maxCountEnv, 10) : 1; + if (isNaN(maxCount) || maxCount < 1) { + core.setFailed(`Invalid max value: ${maxCountEnv}. Must be a positive integer`); + return; + } + core.info(`Max count: ${maxCount}`); + const itemsToProcess = assignItems.slice(0, maxCount); + if (assignItems.length > maxCount) { + core.warning(`Found ${assignItems.length} agent assignments, but max is ${maxCount}. Processing first ${maxCount}.`); + } + const targetRepoEnv = process.env.GH_AW_TARGET_REPO?.trim(); + let targetOwner = context.repo.owner; + let targetRepo = context.repo.repo; + if (targetRepoEnv) { + const parts = targetRepoEnv.split("/"); + if (parts.length === 2) { + targetOwner = parts[0]; + targetRepo = parts[1]; + core.info(`Using target repository: ${targetOwner}/${targetRepo}`); + } else { + core.warning(`Invalid target-repo format: ${targetRepoEnv}. Expected owner/repo. Using current repository.`); + } + } + const agentCache = {}; + const results = []; + for (const item of itemsToProcess) { + const issueNumber = typeof item.issue_number === "number" ? item.issue_number : parseInt(String(item.issue_number), 10); + const agentName = item.agent || defaultAgent; + if (isNaN(issueNumber) || issueNumber <= 0) { + core.error(`Invalid issue_number: ${item.issue_number}`); + continue; + } + if (!AGENT_LOGIN_NAMES[agentName]) { + core.warning(`Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: `Unsupported agent: ${agentName}`, + }); + continue; + } + try { + let agentId = agentCache[agentName]; + if (!agentId) { + core.info(`Looking for ${agentName} coding agent...`); + agentId = await findAgent(targetOwner, targetRepo, agentName); + if (!agentId) { + throw new Error(`${agentName} coding agent is not available for this repository`); + } + agentCache[agentName] = agentId; + core.info(`Found ${agentName} coding agent (ID: ${agentId})`); + } + core.info("Getting issue details..."); + const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber); + if (!issueDetails) { + throw new Error("Failed to get issue details"); + } + core.info(`Issue ID: ${issueDetails.issueId}`); + if (issueDetails.currentAssignees.includes(agentId)) { + core.info(`${agentName} is already assigned to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + continue; + } + core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`); + const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName); + if (!success) { + throw new Error(`Failed to assign ${agentName} via GraphQL`); + } + core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: true, + }); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("coding agent is not available for this repository")) { + try { + const available = await getAvailableAgentLogins(targetOwner, targetRepo); + if (available.length > 0) { + errorMessage += ` (available agents: ${available.join(", ")})`; + } + } catch (e) { + core.debug("Failed to enrich unavailable agent message with available list"); + } + } + core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`); + results.push({ + issue_number: issueNumber, + agent: agentName, + success: false, + error: errorMessage, + }); + } + } + const successCount = results.filter(r => r.success).length; + const failureCount = results.filter(r => !r.success).length; + let summaryContent = "## Agent Assignment\n\n"; + if (successCount > 0) { + summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`; + for (const result of results.filter(r => r.success)) { + summaryContent += `- Issue #${result.issue_number} → Agent: ${result.agent}\n`; + } + summaryContent += "\n"; + } + if (failureCount > 0) { + summaryContent += `❌ Failed to assign ${failureCount} agent(s):\n\n`; + for (const result of results.filter(r => !r.success)) { + summaryContent += `- Issue #${result.issue_number} → Agent: ${result.agent}: ${result.error}\n`; + } + const hasPermissionError = results.some( + r => !r.success && r.error && (r.error.includes("Resource not accessible") || r.error.includes("Insufficient permissions")) + ); + if (hasPermissionError) { + summaryContent += "\n### ⚠️ Permission Requirements\n\n"; + summaryContent += "Assigning Copilot agents requires **ALL** of these permissions:\n\n"; + summaryContent += "```yaml\n"; + summaryContent += "permissions:\n"; + summaryContent += " actions: write\n"; + summaryContent += " contents: write\n"; + summaryContent += " issues: write\n"; + summaryContent += " pull-requests: write\n"; + summaryContent += "```\n\n"; + summaryContent += "**Token capability note:**\n"; + summaryContent += "- Current token (PAT or GITHUB_TOKEN) lacks assignee mutation capability for this repository.\n"; + summaryContent += "- Both `replaceActorsForAssignable` and fallback `addAssigneesToAssignable` returned FORBIDDEN/Resource not accessible.\n"; + summaryContent += "- This typically means bot/user assignment requires an elevated OAuth or GitHub App installation token.\n\n"; + summaryContent += "**Recommended remediation paths:**\n"; + summaryContent += "1. Create & install a GitHub App with: Issues/Pull requests/Contents/Actions (write) → use installation token in job.\n"; + summaryContent += "2. Manual assignment: add the agent through the UI until broader token support is available.\n"; + summaryContent += "3. Open a support ticket referencing failing mutation `replaceActorsForAssignable` and repository slug.\n\n"; + summaryContent += "**Why this failed:** Fine-grained and classic PATs can update issue title (verified) but not modify assignees in this environment.\n\n"; + summaryContent += "📖 Reference: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr (general agent docs)\n"; + } + } + await core.summary.addRaw(summaryContent).write(); + const assignedAgents = results + .filter(r => r.success) + .map(r => `${r.issue_number}:${r.agent}`) + .join("\n"); + core.setOutput("assigned_agents", assignedAgents); + if (failureCount > 0) { + core.setFailed(`Failed to assign ${failureCount} agent(s)`); + } + } + (async () => { + await main(); + })(); + conclusion: needs: - agent - activation + - assign_to_agent - missing_tool - update_project if: (always()) && (needs.agent.result != 'skipped') diff --git a/.github/workflows/ai-triage-campaign.md b/.github/workflows/ai-triage-campaign.md index 8ea6139ef6c..9e68d585829 100644 --- a/.github/workflows/ai-triage-campaign.md +++ b/.github/workflows/ai-triage-campaign.md @@ -19,23 +19,17 @@ on: permissions: contents: read issues: read - repository-projects: write - -# Important: GITHUB_TOKEN cannot access private user projects or organization projects -# You MUST create a PAT with 'project' scope and add it as a repository secret -# Create PAT at: https://github.com/settings/tokens/new?scopes=project&description=Agentic%20Workflows%20Project%20Access engine: copilot tools: github: - mode: local - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} toolsets: [repos, issues] safe-outputs: update-project: max: 20 github-token: ${{ secrets.PROJECT_PAT || secrets.GITHUB_TOKEN }} - missing-tool: + assign-to-agent: + name: copilot --- You are an AI-focused issue triage bot that identifies issues AI agents can solve efficiently and routes them appropriately. @@ -43,8 +37,9 @@ You are an AI-focused issue triage bot that identifies issues AI agents can solv ## Your Mission 1. **Fetch open issues** - Query for open issues in this repository (max ${{ github.event.inputs.max_issues }} most recent, default: 10) -2. **Analyze each issue** - Determine if it's well-suited for AI agent resolution -3. **Route to project board** - Add each issue to project ${{ github.event.inputs.project_url }} with intelligent field assignments +2. **Filter unassigned issues** - Skip issues that already have assignees (do NOT add them to the project board) +3. **Analyze unassigned issues** - Determine if each unassigned issue is well-suited for AI agent resolution +4. **Route to project board** - Add only unassigned issues to project ${{ github.event.inputs.project_url }} with intelligent field assignments ## AI Agent Suitability Assessment @@ -204,11 +199,21 @@ Example for issue #5: ## Assignment Strategy **Immediately assign @copilot when:** +- Issue is currently **unassigned** (no existing assignees) - AI-Readiness Score ≥ 9 - Issue has clear acceptance criteria - All context is provided - No external dependencies +**Action:** Output an `assign_to_agent` safe-output item for these high-readiness issues: +```json +{ + "type": "assign_to_agent", + "issue_number": 123, + "agent": "copilot" +} +``` + **For lower scores (5-8):** - Route to "AI Agent Potential" board - Don't assign yet - needs clarification first @@ -243,7 +248,7 @@ For each issue, provide: - Priority: [priority + brief reason] 3. **Assignment Decision** - - If score ≥ 9: "Assigning to @copilot for immediate work" + - If score ≥ 9: "Assigning to @copilot for immediate work" + output assign_to_agent - If score 5-8: "Needs [specific clarifications] before assignment" - If score < 5: "Requires human review - [specific reasons]" @@ -258,13 +263,15 @@ For each issue, provide: ## Workflow Steps 1. **Fetch Issues**: Use GitHub MCP to query up to ${{ github.event.inputs.max_issues }} most recent open issues (default: 10) -2. **Score Each Issue**: Evaluate AI-readiness based on the criteria above -3. **Route to Project Board**: For each issue, output an `update_project` safe-output item with `"project": "${{ github.event.inputs.project_url }}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments +2. **Filter Unassigned**: Skip any issues that already have assignees (human or agent) - do NOT process or add them to the project board +3. **Score Each Issue**: Evaluate AI-readiness based on the criteria above (only for unassigned issues) +4. **Route to Project Board**: For each unassigned issue, output an `update_project` safe-output item with `"project": "${{ github.event.inputs.project_url }}"` (or `"project": "https://github.com/orgs/githubnext/projects/53"` when the input is empty) to add it to the project board with field assignments +5. **Assign High-Readiness Issues**: For unassigned issues with AI-Readiness Score ≥ 9, output an `assign_to_agent` safe-output item to immediately assign the issue to @copilot ## Execution Notes - This workflow runs every 4 hours automatically (or manually with custom parameters) - Input defaults: max_issues=10, project_url=https://github.com/orgs/githubnext/projects/53 -- All issues are routed to the project board with differentiation via Status field +- **Only unassigned issues** are routed to the project board (issues with existing assignees are completely skipped); routed issues are differentiated via the Status field - Custom fields are created automatically if they don't exist - User projects must exist before workflow runs (cannot auto-create) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index bedc034e234..00719b7f424 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -39,6 +39,7 @@ This declares that the workflow should create at most one new issue. | **Create Discussion** | `create-discussion:` | Create GitHub discussions | 1 | ✅ | | **Close Discussion** | `close-discussion:` | Close discussions with comment and resolution | 1 | ✅ | | **Create Agent Task** | `create-agent-task:` | Create Copilot agent tasks | 1 | ✅ | +| **Assign to Agent** | `assign-to-agent:` | Assign Copilot agents to issues | 1 | ✅ | | **Push to PR Branch** | `push-to-pull-request-branch:` | Push changes to PR branch | 1 | ❌ | | **Update Release** | `update-release:` | Update GitHub release descriptions | 1 | ✅ | | **Code Scanning Alerts** | `create-code-scanning-alert:` | Generate SARIF security advisories | unlimited | ❌ | @@ -344,6 +345,57 @@ safe-outputs: target-repo: "owner/repo" # cross-repository ``` +### Assign to Agent (`assign-to-agent:`) + +Assigns the GitHub Copilot coding agent to issues. The generated job automatically receives the necessary workflow permissions, you only need to provide a token with agent assignment scope. + +```yaml wrap +safe-outputs: + assign-to-agent: + name: "copilot" + target-repo: "owner/repo" # for cross-repository only +``` + +**Token Requirements:** + +The GitHub Action lacks permissions to assign agents. Create a fine-grained personal access token with these permissions and store it as the `GH_AW_AGENT_TOKEN` secret: + +- **Read** access to metadata +- **Write** access to actions, contents, issues, and pull requests + +```yaml wrap +safe-outputs: + assign-to-agent: +``` + +Alternatively, use a GitHub App installation token or override with `github-token`: + +```yaml wrap +safe-outputs: + app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + assign-to-agent: +``` + +**Agent Output Format:** +```json +{ + "type": "assign_to_agent", + "issue_number": 123, + "agent": "copilot" +} +``` + +**Supported Agents:** +- `copilot` - GitHub Copilot coding agent (`copilot-swe-agent`) + +**Repository Settings:** + +Ensure Copilot is enabled for your repository. Check organization settings if bot assignments are restricted. + +Reference: [GitHub Copilot agent documentation](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/) + ## Cross-Repository Operations Many safe outputs support `target-repo` for cross-repository operations. Requires a PAT (via `github-token` or `GH_AW_GITHUB_TOKEN`) with access to target repositories. The default `GITHUB_TOKEN` only has permissions for the current repository. diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index c2111f97302..17de28f7892 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -38,7 +38,8 @@ func (c *Compiler) buildAssignToAgentJob(data *WorkflowData, mainJobName string) // Pass the max limit customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_MAX_COUNT: %d\n", maxCount)) - // Pass the GitHub token as GH_TOKEN for gh CLI (falls back to GITHUB_TOKEN) + // Pass the GitHub token for GraphQL agent assignment mutation + // Prioritize GH_AW_AGENT_TOKEN since it needs specific permissions (actions, contents, issues, pull-requests write) var tokenValue string if data.SafeOutputs.AssignToAgent.GitHubToken != "" { tokenValue = data.SafeOutputs.AssignToAgent.GitHubToken @@ -47,7 +48,7 @@ func (c *Compiler) buildAssignToAgentJob(data *WorkflowData, mainJobName string) } else if data.GitHubToken != "" { tokenValue = data.GitHubToken } else { - tokenValue = "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" + tokenValue = "${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN }}" } customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_TOKEN: %s\n", tokenValue)) diff --git a/pkg/workflow/js/assign_to_agent.cjs b/pkg/workflow/js/assign_to_agent.cjs index 2843a6488fa..e5b9b0ea7d4 100644 --- a/pkg/workflow/js/assign_to_agent.cjs +++ b/pkg/workflow/js/assign_to_agent.cjs @@ -8,9 +8,7 @@ const { generateStagedPreview } = require("./staged_preview.cjs"); * Map agent names to their GitHub bot login names */ const AGENT_LOGIN_NAMES = { - copilot: "copilot-swe-agent", - claude: "claude-swe-agent", - codex: "codex-swe-agent", + copilot: "copilot-swe-agent" }; /** @@ -52,7 +50,7 @@ async function getAvailableAgentLogins(owner, repo) { * Find an agent in repository's suggested actors using GraphQL * @param {string} owner - Repository owner * @param {string} repo - Repository name - * @param {string} agentName - Agent name (copilot, claude, codex) + * @param {string} agentName - Agent name (copilot) * @returns {Promise} Agent ID or null if not found */ async function findAgent(owner, repo, agentName) { @@ -183,6 +181,8 @@ async function assignAgentToIssue(issueId, agentId, currentAssignees, agentName) `; try { + // Uses GH_TOKEN which is set to GH_AW_AGENT_TOKEN (or fallback) by the job + // This token needs: Read metadata, Write actions/contents/issues/pull-requests const response = await github.graphql(mutation); if (response.replaceActorsForAssignable && response.replaceActorsForAssignable.__typename) {