Skip to content

Commit 5980640

Browse files
committed
ci: add PR category check workflow with auto-labeling
- Add PR Category section to PR template with 5 categories (Bug Fix, Feature, Performance, Tests, Documentation) - Add GitHub Actions workflow that validates PR category selection - Auto-apply matching repo labels (Issue-Bug, Issue-Enhancement, Issue-Performance, Issue-Testing, Issue-Documentation) - Auto-remove labels when categories are unchecked - Uses pull_request_target for fork PR compatibility
1 parent 0967857 commit 5980640

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

.github/PULL_REQUEST_TEMPLATE/generic.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ about: Submit changes to the project for review and inclusion
1515

1616
This PR fixes #
1717

18+
## PR Category
19+
20+
<!--- ⚠️ CI ENFORCED: You MUST check at least ONE category below or the CI will fail. -->
21+
<!--- Check all categories that apply to this pull request. -->
22+
23+
- [ ] 🐛 Bug Fix — Fixes a bug or incorrect behavior
24+
- [ ] ✨ Feature — Adds new functionality
25+
- [ ] ⚡ Performance — Improves performance (load time, memory, rendering, etc.)
26+
- [ ] 🧪 Tests — Adds or updates test coverage
27+
- [ ] 📝 Documentation — Updates to docs, comments, or README
28+
1829
## Changes Made
1930

2031
<!--- Provide a summary of the changes made in this pull request. -->
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: PR Category Check
2+
3+
# Ensures every PR has at least one category checkbox checked.
4+
# Automatically applies matching GitHub labels to the PR.
5+
# Uses pull_request_target so it works for fork PRs too (runs in base repo context).
6+
# This is safe because we only read the PR body from the event payload — no fork code is checked out or executed.
7+
8+
on:
9+
pull_request_target:
10+
types: [opened, edited, synchronize, reopened]
11+
12+
permissions:
13+
pull-requests: write
14+
contents: read
15+
16+
jobs:
17+
check-pr-category:
18+
name: Validate PR Category & Auto-Label
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Check categories and apply labels
22+
uses: actions/github-script@v7
23+
with:
24+
script: |
25+
const prBody = context.payload.pull_request.body || '';
26+
const prNumber = context.payload.pull_request.number;
27+
28+
// Category checkboxes mapped to their GitHub label names
29+
const categories = [
30+
{ label: 'Issue-Bug', emoji: '🐛', displayName: '🐛 Bug Fix', pattern: /- \[x\]\s*🐛\s*Bug Fix/i },
31+
{ label: 'Issue-Enhancement', emoji: '✨', displayName: '✨ Feature', pattern: /- \[x\]\s*✨\s*Feature/i },
32+
{ label: 'Issue-Performance', emoji: '⚡', displayName: '⚡ Performance', pattern: /- \[x\]\s*⚡\s*Performance/i },
33+
{ label: 'Issue-Testing', emoji: '🧪', displayName: '🧪 Tests', pattern: /- \[x\]\s*🧪\s*Tests/i },
34+
{ label: 'Issue-Documentation', emoji: '📝', displayName: '📝 Documentation', pattern: /- \[x\]\s*📝\s*Documentation/i },
35+
];
36+
37+
const checkedCategories = categories.filter(cat => cat.pattern.test(prBody));
38+
const uncheckedCategories = categories.filter(cat => !cat.pattern.test(prBody));
39+
40+
// --- Step 1: Fail CI if no category is selected ---
41+
if (checkedCategories.length === 0) {
42+
const message = [
43+
'## ❌ PR Category Required',
44+
'',
45+
'This pull request does not have any **PR Category** selected.',
46+
'Please edit your PR description and check **at least one** category checkbox:',
47+
'',
48+
'| Category | Description |',
49+
'|----------|-------------|',
50+
'| 🐛 Bug Fix | Fixes a bug or incorrect behavior |',
51+
'| ✨ Feature | Adds new functionality |',
52+
'| ⚡ Performance | Improves performance |',
53+
'| 🧪 Tests | Adds or updates test coverage |',
54+
'| 📝 Documentation | Updates to docs, comments, or README |',
55+
'',
56+
'Example: Change `- [ ] 🐛 Bug Fix` to `- [x] 🐛 Bug Fix`',
57+
'',
58+
'> **Tip:** You can select multiple categories if your PR spans several areas.',
59+
].join('\n');
60+
61+
core.setFailed(message);
62+
return;
63+
}
64+
65+
// --- Step 2: Auto-apply labels for checked categories ---
66+
const labelsToAdd = checkedCategories.map(cat => cat.label);
67+
const labelsToRemove = uncheckedCategories.map(cat => cat.label);
68+
69+
// Get current labels on the PR
70+
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
issue_number: prNumber,
74+
});
75+
const currentLabelNames = currentLabels.map(l => l.name);
76+
77+
// Add labels that are checked but not yet on the PR
78+
for (const label of labelsToAdd) {
79+
if (!currentLabelNames.includes(label)) {
80+
try {
81+
await github.rest.issues.addLabels({
82+
owner: context.repo.owner,
83+
repo: context.repo.repo,
84+
issue_number: prNumber,
85+
labels: [label],
86+
});
87+
core.info(`🏷️ Added label: "${label}"`);
88+
} catch (error) {
89+
core.warning(`⚠️ Could not add label "${label}". Make sure it exists in the repo. Error: ${error.message}`);
90+
}
91+
}
92+
}
93+
94+
// Remove labels that are unchecked but still on the PR
95+
for (const label of labelsToRemove) {
96+
if (currentLabelNames.includes(label)) {
97+
try {
98+
await github.rest.issues.removeLabel({
99+
owner: context.repo.owner,
100+
repo: context.repo.repo,
101+
issue_number: prNumber,
102+
name: label,
103+
});
104+
core.info(`🗑️ Removed label: "${label}"`);
105+
} catch (error) {
106+
core.warning(`⚠️ Could not remove label "${label}". Error: ${error.message}`);
107+
}
108+
}
109+
}
110+
111+
const selected = checkedCategories.map(c => c.displayName).join(', ');
112+
core.info(`✅ PR categories selected: ${selected}`);
113+
core.info(`🏷️ Labels synced: ${labelsToAdd.join(', ')}`);

0 commit comments

Comments
 (0)