squad-label-enforce.yml (7032B)
1 name: Squad Label Enforce 2 3 on: 4 issues: 5 types: [labeled] 6 7 permissions: 8 issues: write 9 contents: read 10 11 jobs: 12 enforce: 13 runs-on: ubuntu-latest 14 steps: 15 - uses: actions/checkout@v4 16 17 - name: Enforce mutual exclusivity 18 uses: actions/github-script@v7 19 with: 20 script: | 21 const issue = context.payload.issue; 22 const appliedLabel = context.payload.label.name; 23 24 // Namespaces with mutual exclusivity rules 25 const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; 26 27 // Skip if not a managed namespace label 28 if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { 29 core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`); 30 return; 31 } 32 33 const allLabels = issue.labels.map(l => l.name); 34 35 // Handle go: namespace (mutual exclusivity) 36 if (appliedLabel.startsWith('go:')) { 37 const otherGoLabels = allLabels.filter(l => 38 l.startsWith('go:') && l !== appliedLabel 39 ); 40 41 if (otherGoLabels.length > 0) { 42 // Remove conflicting go: labels 43 for (const label of otherGoLabels) { 44 await github.rest.issues.removeLabel({ 45 owner: context.repo.owner, 46 repo: context.repo.repo, 47 issue_number: issue.number, 48 name: label 49 }); 50 core.info(`Removed conflicting label: ${label}`); 51 } 52 53 // Post update comment 54 await github.rest.issues.createComment({ 55 owner: context.repo.owner, 56 repo: context.repo.repo, 57 issue_number: issue.number, 58 body: `🏷️ Triage verdict updated → \`${appliedLabel}\`` 59 }); 60 } 61 62 // Auto-apply release:backlog if go:yes and no release target 63 if (appliedLabel === 'go:yes') { 64 const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); 65 if (!hasReleaseLabel) { 66 await github.rest.issues.addLabels({ 67 owner: context.repo.owner, 68 repo: context.repo.repo, 69 issue_number: issue.number, 70 labels: ['release:backlog'] 71 }); 72 73 await github.rest.issues.createComment({ 74 owner: context.repo.owner, 75 repo: context.repo.repo, 76 issue_number: issue.number, 77 body: `📋 Marked as \`release:backlog\` — assign a release target when ready.` 78 }); 79 80 core.info('Applied release:backlog for go:yes issue'); 81 } 82 } 83 84 // Remove release: labels if go:no 85 if (appliedLabel === 'go:no') { 86 const releaseLabels = allLabels.filter(l => l.startsWith('release:')); 87 if (releaseLabels.length > 0) { 88 for (const label of releaseLabels) { 89 await github.rest.issues.removeLabel({ 90 owner: context.repo.owner, 91 repo: context.repo.repo, 92 issue_number: issue.number, 93 name: label 94 }); 95 core.info(`Removed release label from go:no issue: ${label}`); 96 } 97 } 98 } 99 } 100 101 // Handle release: namespace (mutual exclusivity) 102 if (appliedLabel.startsWith('release:')) { 103 const otherReleaseLabels = allLabels.filter(l => 104 l.startsWith('release:') && l !== appliedLabel 105 ); 106 107 if (otherReleaseLabels.length > 0) { 108 // Remove conflicting release: labels 109 for (const label of otherReleaseLabels) { 110 await github.rest.issues.removeLabel({ 111 owner: context.repo.owner, 112 repo: context.repo.repo, 113 issue_number: issue.number, 114 name: label 115 }); 116 core.info(`Removed conflicting label: ${label}`); 117 } 118 119 // Post update comment 120 await github.rest.issues.createComment({ 121 owner: context.repo.owner, 122 repo: context.repo.repo, 123 issue_number: issue.number, 124 body: `🏷️ Release target updated → \`${appliedLabel}\`` 125 }); 126 } 127 } 128 129 // Handle type: namespace (mutual exclusivity) 130 if (appliedLabel.startsWith('type:')) { 131 const otherTypeLabels = allLabels.filter(l => 132 l.startsWith('type:') && l !== appliedLabel 133 ); 134 135 if (otherTypeLabels.length > 0) { 136 for (const label of otherTypeLabels) { 137 await github.rest.issues.removeLabel({ 138 owner: context.repo.owner, 139 repo: context.repo.repo, 140 issue_number: issue.number, 141 name: label 142 }); 143 core.info(`Removed conflicting label: ${label}`); 144 } 145 146 await github.rest.issues.createComment({ 147 owner: context.repo.owner, 148 repo: context.repo.repo, 149 issue_number: issue.number, 150 body: `🏷️ Issue type updated → \`${appliedLabel}\`` 151 }); 152 } 153 } 154 155 // Handle priority: namespace (mutual exclusivity) 156 if (appliedLabel.startsWith('priority:')) { 157 const otherPriorityLabels = allLabels.filter(l => 158 l.startsWith('priority:') && l !== appliedLabel 159 ); 160 161 if (otherPriorityLabels.length > 0) { 162 for (const label of otherPriorityLabels) { 163 await github.rest.issues.removeLabel({ 164 owner: context.repo.owner, 165 repo: context.repo.repo, 166 issue_number: issue.number, 167 name: label 168 }); 169 core.info(`Removed conflicting label: ${label}`); 170 } 171 172 await github.rest.issues.createComment({ 173 owner: context.repo.owner, 174 repo: context.repo.repo, 175 issue_number: issue.number, 176 body: `🏷️ Priority updated → \`${appliedLabel}\`` 177 }); 178 } 179 } 180 181 core.info(`Label enforcement complete for ${appliedLabel}`);