Skip to content

Commit 49fd78a

Browse files
committed
chore(actions): Add PR template enforcement workflow
1 parent db18056 commit 49fd78a

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: Enforce PR Template
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, edited, synchronize, reopened]
6+
workflow_dispatch:
7+
inputs:
8+
pr_number:
9+
description: 'Pull request number to validate'
10+
required: true
11+
type: number
12+
13+
permissions:
14+
pull-requests: write
15+
contents: read
16+
issues: write
17+
18+
jobs:
19+
enforce-template:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Determine PR number
24+
id: pr
25+
run: |
26+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
27+
echo "number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
28+
else
29+
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
30+
fi
31+
32+
- name: Enforce template
33+
uses: actions/github-script@v8
34+
with:
35+
github-token: ${{ secrets.GITHUB_TOKEN }}
36+
script: |
37+
const prNumber = Number('${{ steps.pr.outputs.number }}');
38+
const org = 'Pycord-Development';
39+
const INVALID_LABEL = 'invalid';
40+
41+
// 1) Load PR
42+
const { data: pr } = await github.rest.pulls.get({
43+
owner: context.repo.owner,
44+
repo: context.repo.repo,
45+
pull_number: prNumber,
46+
});
47+
48+
const author = pr.user.login;
49+
50+
// 2) Check org membership
51+
let isMember = false;
52+
try {
53+
await github.rest.orgs.checkMembershipForUser({ org, username: author });
54+
isMember = true;
55+
} catch (error) {
56+
if (error.status !== 404) {
57+
throw error;
58+
}
59+
}
60+
61+
if (isMember) {
62+
core.info(`Author ${author} is in org ${org}; skipping enforcement.`);
63+
return;
64+
}
65+
66+
// 3) Validate body
67+
const body = (pr.body || '').trim();
68+
const problems = [];
69+
70+
// Basic length check – tune as needed
71+
if (body.length < 150) {
72+
problems.push('PR description is too short (expected more content based on the template).');
73+
}
74+
75+
// Required headings from PULL_REQUEST_TEMPLATE.md
76+
const requiredHeadings = [
77+
'## Summary',
78+
'## Information',
79+
'## Checklist',
80+
];
81+
82+
for (const heading of requiredHeadings) {
83+
if (!body.includes(heading)) {
84+
problems.push(`Missing required section: "${heading}"`);
85+
}
86+
}
87+
88+
// Enforce AI disclosure line
89+
if (!body.includes('AI Usage has been disclosed')) {
90+
problems.push('The line "AI Usage has been disclosed." is missing.');
91+
}
92+
93+
if (problems.length === 0) {
94+
core.info('PR body passed template validation.');
95+
return;
96+
}
97+
98+
// 4) If invalid: label, comment, close, and fail
99+
core.info('Template validation failed. Applying actions.');
100+
101+
const existingLabels = (pr.labels || []).map(l => l.name);
102+
if (!existingLabels.includes(INVALID_LABEL)) {
103+
await github.rest.issues.addLabels({
104+
owner: context.repo.owner,
105+
repo: context.repo.repo,
106+
issue_number: prNumber,
107+
labels: [INVALID_LABEL],
108+
});
109+
}
110+
111+
const problemsBlock = problems.join('\n');
112+
113+
await github.rest.issues.createComment({
114+
owner: context.repo.owner,
115+
repo: context.repo.repo,
116+
issue_number: prNumber,
117+
body: [
118+
'This pull request does not follow the required pull request template.',
119+
'',
120+
'Please use the default template (`PULL_REQUEST_TEMPLATE.md`) and fill out all required sections.',
121+
'',
122+
'Problems detected:',
123+
'',
124+
'```',
125+
problemsBlock,
126+
'```',
127+
].join('\n'),
128+
});
129+
130+
if (pr.state === 'open') {
131+
await github.rest.pulls.update({
132+
owner: context.repo.owner,
133+
repo: context.repo.repo,
134+
pull_number: prNumber,
135+
state: 'closed',
136+
});
137+
}
138+
139+
core.setFailed('PR does not follow the required template.');

0 commit comments

Comments
 (0)