Setting the file. One moment. Sync Squad Labels · {skill Name} · github/gh-aw · Skills Docsworkflows/sync-squad-labels.yml
workflows/sync-squad-labels.yml
YAML·221 lines·9 KB
14permissions:
15 issues: write
16 contents: read
17
18jobs:
19 sync-labels:
20 runs-on: ubuntu-latest
21 steps:
22 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
23
24 - name: Parse roster and sync labels
25 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9
26 with:
27 script: |
28 const fs = require('fs');
29 let teamFile = '.squad/team.md';
30 if (!fs.existsSync(teamFile)) {
31 teamFile = '.ai-team/team.md';
32 }
33
34 if (!fs.existsSync(teamFile)) {
35 core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync');
36 return;
37 }
38
39 const content = fs.readFileSync(teamFile, 'utf8');
40 const lines = content.split('\n');
41
42 // Parse the Members table for agent names
43 const members = [];
44 let inMembersTable = false;
45 for (const line of lines) {
46 if (line.match(/^##\s+(Members|Team Roster)/i)) {
47 inMembersTable = true;
48 continue;
49 }
50 if (inMembersTable && line.startsWith('## ')) {
51 break;
52 }
53 if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
54 const cells = line.split('|').map(c => c.trim()).filter(Boolean);
55 if (cells.length >= 2 && cells[0] !== 'Scribe') {
56 members.push({
57 name: cells[0],
58 role: cells[1]
59 });
60 }
61 }
62 }
63
64 core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`);
65
66 // Check if @copilot is on the team
67 const hasCopilot = content.includes('🤖 Coding Agent');
68
69 // Define label color palette for squad labels
70 const SQUAD_COLOR = '9B8FCC';
71 const COPILOT_COLOR = '10b981';
72
73 // Curated palette of visually distinct colors for squad member labels.
74 // These have good contrast with white text and avoid colors used by
75 // other label groups (go:*, release:*, type:*, priority:*, status:*).
76 const SQUAD_MEMBER_PALETTE = [
77 'E06C75', // soft red
78 'E5C07B', // gold
79 '56B6C2', // teal
80 'C678DD', // purple
81 '61AFEF', // blue
82 '98C379', // green
83 'D19A66', // orange
84 'BE5046', // rust
85 '7EC8E3', // sky blue
86 '9ECE6A', // lime
87 'F7768E', // pink
88 'FF9E64', // tangerine
89 '7AA2F7', // periwinkle
90 'BB9AF7', // lavender
91 '73DACA', // mint
92 '2AC3DE', // cyan
93 'DB4B4B', // crimson
94 'E0AF68', // amber
95 '449DAB', // dark teal
96 'A9DC76', // chartreuse
97 ];
98
99 // Deterministic color from palette based on agent slug hash
100 const usedColors = new Set();
101
102 function getAgentColor(slug) {
103 let hash = 0;
104 for (const char of slug) {
105 hash = ((hash << 5) - hash) + char.charCodeAt(0);
106 hash = hash & hash;
107 }
108 let index = Math.abs(hash) % SQUAD_MEMBER_PALETTE.length;
109 // Linear probe to avoid collisions
110 let attempts = 0;
111 while (usedColors.has(SQUAD_MEMBER_PALETTE[index]) && attempts < SQUAD_MEMBER_PALETTE.length) {
112 index = (index + 1) % SQUAD_MEMBER_PALETTE.length;
113 attempts++;
114 }
115 usedColors.add(SQUAD_MEMBER_PALETTE[index]);
116 return SQUAD_MEMBER_PALETTE[index];
117 }
118
119 // Define go: and release: labels (static)
120 const GO_LABELS = [
121 { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' },
122 { name: 'go:no', color: 'B60205', description: 'Not pursuing' },
123 { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' }
124 ];
125
126 const RELEASE_LABELS = [
127 { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' },
128 { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' },
129 { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' },
130 { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' },
131 { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' }
132 ];
133
134 const TYPE_LABELS = [
135 { name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
136 { name: 'type:bug', color: 'FF0422', description: 'Something broken' },
137 { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' },
138 { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' },
139 { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' },
140 { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }
141 ];
142
143 // High-signal labels — these MUST visually dominate all others
144 const SIGNAL_LABELS = [
145 { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' },
146 { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' }
147 ];
148
149 const PRIORITY_LABELS = [
150 { name: 'priority:p0', color: 'B60205', description: 'Blocking release' },
151 { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' },
152 { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' }
153 ];
154
155 function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); }
156
157 // Ensure the base "squad" triage label exists
158 const labels = [
159 { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' }
160 ];
161
162 for (const member of members) {
163 const slug = slugify(member.name);
164 labels.push({
165 name: `squad:${slug}`,
166 color: getAgentColor(slug),
167 description: `Assigned to ${member.name} (${member.role})`
168 });
169 }
170
171 // Add @copilot label if coding agent is on the team
172 if (hasCopilot) {
173 labels.push({
174 name: 'squad:copilot',
175 color: COPILOT_COLOR,
176 description: 'Assigned to @copilot (Coding Agent) for autonomous work'
177 });
178 }
179
180 // Add go:, release:, type:, priority:, and high-signal labels
181 labels.push(...GO_LABELS);
182 labels.push(...RELEASE_LABELS);
183 labels.push(...TYPE_LABELS);
184 labels.push(...PRIORITY_LABELS);
185 labels.push(...SIGNAL_LABELS);
186
187 // Sync labels (create or update)
188 for (const label of labels) {
189 try {
190 await github.rest.issues.getLabel({
191 owner: context.repo.owner,
192 repo: context.repo.repo,
193 name: label.name
194 });
195 // Label exists — update it
196 await github.rest.issues.updateLabel({
197 owner: context.repo.owner,
198 repo: context.repo.repo,
199 name: label.name,
200 color: label.color,
201 description: label.description
202 });
203 core.info(`Updated label: ${label.name}`);
204 } catch (err) {
205 if (err.status === 404) {
206 // Label doesn't exist — create it
207 await github.rest.issues.createLabel({
208 owner: context.repo.owner,
209 repo: context.repo.repo,
210 name: label.name,
211 color: label.color,
212 description: label.description
213 });
214 core.info(`Created label: ${label.name}`);
215 } else {
216 throw err;
217 }
218 }
219 }
220
221 core.info(`Label sync complete: ${labels.length} labels synced`);