main
yml 161 lines 6.27 KB
Raw
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 }