diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..f7a6328 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,96 @@ +name: Auto Labeler via PR Description + +on: + # pull_request_target is required to write labels on PRs from forks. + pull_request_target: + types: [opened, edited, synchronize, reopened, ready_for_review] + +jobs: + apply-labels: + runs-on: ubuntu-latest + permissions: + pull-requests: read + issues: write # PR labels are managed via the Issues API. + + steps: + - name: Read description and apply labels + uses: actions/github-script@v7 + with: + script: | + // Only apply labels that exist in .github/pull_request_template.md + // Change type (choose one release label from taxonomy): + const allowedReleaseLabels = new Set([ + 'release:major', + 'release:feature', + 'release:patterns', + 'release:api', + 'release:data', + 'release:privacy', + 'release:fix', + 'release:docs', + 'release:infra', + 'release:security', + ]); + + const body = context.payload.pull_request.body || ''; + if (!body.trim()) return; + + // Matches: - [x] `release:major` + const checkedReleaseLabelRegex = /\[x\]\s*`(release:[^`]+)`/gi; + const selected = []; + let match; + while ((match = checkedReleaseLabelRegex.exec(body)) !== null) { + const label = (match[1] || '').trim(); + if (allowedReleaseLabels.has(label)) selected.push(label); + } + + const uniqueSelected = [...new Set(selected)]; + if (uniqueSelected.length === 0) { + console.log('No release label selected in PR template; skipping.'); + return; + } + + if (uniqueSelected.length > 1) { + throw new Error( + `Multiple release labels selected: ${uniqueSelected.join(', ')}. Select exactly one.` + ); + } + + const chosen = uniqueSelected[0]; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.issue.number; + + // Sync: remove other release:* labels from the allowed set, then add the chosen one. + const { data: existing } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number, + }); + + const existingReleaseLabels = existing + .map(l => l.name) + .filter(name => allowedReleaseLabels.has(name)); + + const toRemove = existingReleaseLabels.filter(name => name !== chosen); + for (const name of toRemove) { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name, + }); + console.log(`Removed label: ${name}`); + } + + if (!existingReleaseLabels.includes(chosen)) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [chosen], + }); + console.log(`Added label: ${chosen}`); + } else { + console.log(`Label already present: ${chosen}`); + }