main
yml 171 lines 6.72 KB
Raw
1 name: Sync Squad Labels
2
3 on:
4 push:
5 paths:
6 - '.squad/team.md'
7 - '.ai-team/team.md'
8 workflow_dispatch:
9
10 permissions:
11 issues: write
12 contents: read
13
14 jobs:
15 sync-labels:
16 runs-on: ubuntu-latest
17 steps:
18 - uses: actions/checkout@v4
19
20 - name: Parse roster and sync labels
21 uses: actions/github-script@v7
22 with:
23 script: |
24 const fs = require('fs');
25 let teamFile = '.squad/team.md';
26 if (!fs.existsSync(teamFile)) {
27 teamFile = '.ai-team/team.md';
28 }
29
30 if (!fs.existsSync(teamFile)) {
31 core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync');
32 return;
33 }
34
35 const content = fs.readFileSync(teamFile, 'utf8');
36 const lines = content.split('\n');
37
38 // Parse the Members table for agent names
39 const members = [];
40 let inMembersTable = false;
41 for (const line of lines) {
42 if (line.match(/^##\s+(Members|Team Roster)/i)) {
43 inMembersTable = true;
44 continue;
45 }
46 if (inMembersTable && line.startsWith('## ')) {
47 break;
48 }
49 if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
50 const cells = line.split('|').map(c => c.trim()).filter(Boolean);
51 if (cells.length >= 2 && cells[0] !== 'Scribe') {
52 members.push({
53 name: cells[0],
54 role: cells[1]
55 });
56 }
57 }
58 }
59
60 core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`);
61
62 // Check if @copilot is on the team
63 const hasCopilot = content.includes('🤖 Coding Agent');
64
65 // Define label color palette for squad labels
66 const SQUAD_COLOR = '9B8FCC';
67 const MEMBER_COLOR = '9B8FCC';
68 const COPILOT_COLOR = '10b981';
69
70 // Define go: and release: labels (static)
71 const GO_LABELS = [
72 { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' },
73 { name: 'go:no', color: 'B60205', description: 'Not pursuing' },
74 { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' }
75 ];
76
77 const RELEASE_LABELS = [
78 { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' },
79 { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' },
80 { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' },
81 { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' },
82 { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' }
83 ];
84
85 const TYPE_LABELS = [
86 { name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
87 { name: 'type:bug', color: 'FF0422', description: 'Something broken' },
88 { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' },
89 { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' },
90 { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' },
91 { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }
92 ];
93
94 // High-signal labels — these MUST visually dominate all others
95 const SIGNAL_LABELS = [
96 { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' },
97 { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' }
98 ];
99
100 const PRIORITY_LABELS = [
101 { name: 'priority:p0', color: 'B60205', description: 'Blocking release' },
102 { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' },
103 { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' }
104 ];
105
106 function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); }
107
108 // Ensure the base "squad" triage label exists
109 const labels = [
110 { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' }
111 ];
112
113 for (const member of members) {
114 labels.push({
115 name: `squad:${slugify(member.name)}`,
116 color: MEMBER_COLOR,
117 description: `Assigned to ${member.name} (${member.role})`
118 });
119 }
120
121 // Add @copilot label if coding agent is on the team
122 if (hasCopilot) {
123 labels.push({
124 name: 'squad:copilot',
125 color: COPILOT_COLOR,
126 description: 'Assigned to @copilot (Coding Agent) for autonomous work'
127 });
128 }
129
130 // Add go:, release:, type:, priority:, and high-signal labels
131 labels.push(...GO_LABELS);
132 labels.push(...RELEASE_LABELS);
133 labels.push(...TYPE_LABELS);
134 labels.push(...PRIORITY_LABELS);
135 labels.push(...SIGNAL_LABELS);
136
137 // Sync labels (create or update)
138 for (const label of labels) {
139 try {
140 await github.rest.issues.getLabel({
141 owner: context.repo.owner,
142 repo: context.repo.repo,
143 name: label.name
144 });
145 // Label exists — update it
146 await github.rest.issues.updateLabel({
147 owner: context.repo.owner,
148 repo: context.repo.repo,
149 name: label.name,
150 color: label.color,
151 description: label.description
152 });
153 core.info(`Updated label: ${label.name}`);
154 } catch (err) {
155 if (err.status === 404) {
156 // Label doesn't exist — create it
157 await github.rest.issues.createLabel({
158 owner: context.repo.owner,
159 repo: context.repo.repo,
160 name: label.name,
161 color: label.color,
162 description: label.description
163 });
164 core.info(`Created label: ${label.name}`);
165 } else {
166 throw err;
167 }
168 }
169 }
170
171 core.info(`Label sync complete: ${labels.length} labels synced`);