squad-issue-assign.yml (6585B)
1 name: Squad Issue Assign 2 3 on: 4 issues: 5 types: [labeled] 6 7 permissions: 8 issues: write 9 contents: read 10 11 jobs: 12 assign-work: 13 # Only trigger on squad:{member} labels (not the base "squad" label) 14 if: startsWith(github.event.label.name, 'squad:') 15 runs-on: ubuntu-latest 16 steps: 17 - uses: actions/checkout@v4 18 19 - name: Identify assigned member and trigger work 20 uses: actions/github-script@v7 21 with: 22 script: | 23 const fs = require('fs'); 24 const issue = context.payload.issue; 25 const label = context.payload.label.name; 26 27 // Extract member name from label (e.g., "squad:ripley" → "ripley") 28 const memberName = label.replace('squad:', '').toLowerCase(); 29 30 // Read team roster — check .squad/ first, fall back to .ai-team/ 31 let teamFile = '.squad/team.md'; 32 if (!fs.existsSync(teamFile)) { 33 teamFile = '.ai-team/team.md'; 34 } 35 if (!fs.existsSync(teamFile)) { 36 core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work'); 37 return; 38 } 39 40 const content = fs.readFileSync(teamFile, 'utf8'); 41 const lines = content.split('\n'); 42 43 // Check if this is a coding agent assignment 44 const isCopilotAssignment = memberName === 'copilot'; 45 46 let assignedMember = null; 47 if (isCopilotAssignment) { 48 assignedMember = { name: '@copilot', role: 'Coding Agent' }; 49 } else { 50 let inMembersTable = false; 51 for (const line of lines) { 52 if (line.match(/^##\s+(Members|Team Roster)/i)) { 53 inMembersTable = true; 54 continue; 55 } 56 if (inMembersTable && line.startsWith('## ')) { 57 break; 58 } 59 if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { 60 const cells = line.split('|').map(c => c.trim()).filter(Boolean); 61 if (cells.length >= 2 && cells[0].toLowerCase() === memberName) { 62 assignedMember = { name: cells[0], role: cells[1] }; 63 break; 64 } 65 } 66 } 67 } 68 69 if (!assignedMember) { 70 core.warning(`No member found matching label "${label}"`); 71 await github.rest.issues.createComment({ 72 owner: context.repo.owner, 73 repo: context.repo.repo, 74 issue_number: issue.number, 75 body: `⚠️ No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.` 76 }); 77 return; 78 } 79 80 // Post assignment acknowledgment 81 let comment; 82 if (isCopilotAssignment) { 83 comment = [ 84 `### 🤖 Routed to @copilot (Coding Agent)`, 85 '', 86 `**Issue:** #${issue.number} — ${issue.title}`, 87 '', 88 `@copilot has been assigned and will pick this up automatically.`, 89 '', 90 `> The coding agent will create a \`copilot/*\` branch and open a draft PR.`, 91 `> Review the PR as you would any team member's work.`, 92 ].join('\n'); 93 } else { 94 comment = [ 95 `### 📋 Assigned to ${assignedMember.name} (${assignedMember.role})`, 96 '', 97 `**Issue:** #${issue.number} — ${issue.title}`, 98 '', 99 `${assignedMember.name} will pick this up in the next Copilot session.`, 100 '', 101 `> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`, 102 `> Otherwise, start a Copilot session and say:`, 103 `> \`${assignedMember.name}, work on issue #${issue.number}\``, 104 ].join('\n'); 105 } 106 107 await github.rest.issues.createComment({ 108 owner: context.repo.owner, 109 repo: context.repo.repo, 110 issue_number: issue.number, 111 body: comment 112 }); 113 114 core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`); 115 116 # Separate step: assign @copilot using PAT (required for coding agent) 117 - name: Assign @copilot coding agent 118 if: github.event.label.name == 'squad:copilot' 119 uses: actions/github-script@v7 120 with: 121 github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} 122 script: | 123 const owner = context.repo.owner; 124 const repo = context.repo.repo; 125 const issue_number = context.payload.issue.number; 126 127 // Get the default branch name (main, master, etc.) 128 const { data: repoData } = await github.rest.repos.get({ owner, repo }); 129 const baseBranch = repoData.default_branch; 130 131 try { 132 await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { 133 owner, 134 repo, 135 issue_number, 136 assignees: ['copilot-swe-agent[bot]'], 137 agent_assignment: { 138 target_repo: `${owner}/${repo}`, 139 base_branch: baseBranch, 140 custom_instructions: '', 141 custom_agent: '', 142 model: '' 143 }, 144 headers: { 145 'X-GitHub-Api-Version': '2022-11-28' 146 } 147 }); 148 core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`); 149 } catch (err) { 150 core.warning(`Assignment with agent_assignment failed: ${err.message}`); 151 // Fallback: try without agent_assignment 152 try { 153 await github.rest.issues.addAssignees({ 154 owner, repo, issue_number, 155 assignees: ['copilot-swe-agent'] 156 }); 157 core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`); 158 } catch (err2) { 159 core.warning(`Fallback also failed: ${err2.message}`); 160 } 161 }