| 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`); |