| 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 | } |