-
Notifications
You must be signed in to change notification settings - Fork 2.8k
278 lines (250 loc) · 13.1 KB
/
Copy pathcheck-pr-template.yml
File metadata and controls
278 lines (250 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: Triage pull requests
on:
pull_request_target:
types: [opened, reopened, edited]
schedule:
- cron: '0 8 * * *' # every day at 08:00 UTC
permissions:
pull-requests: write
issues: write
jobs:
# ── 1. On every PR open/edit ─────────────────────────────────────────────────
# a) Detect non-project practice/test submissions → close immediately
# b) Otherwise, check that the PR template was filled in
# Running both checks in the same job ensures they are sequential and never
# post two independent comments on the same PR.
triage-pr:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- name: Ensure required labels exist
uses: actions/github-script@v7
with:
script: |
const labels = [
{ name: 'invalid', color: 'e11d48', description: 'This does not seem right' },
{ name: 'needs-information', color: 'e4e669', description: 'More information is needed before this can be reviewed' },
];
for (const label of labels) {
try {
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, ...label });
} catch (e) {
if (e.status !== 422) throw e; // 422 = already exists, ignore
}
}
- name: Check non-project PR patterns then template completeness
uses: actions/github-script@v7
env:
# Classic PAT with read:org scope (store as repo/org secret ORG_READ_PAT).
# Required to detect *private* org members — GITHUB_TOKEN only sees public ones.
# If the secret is absent the workflow still runs but bypasses public members only.
ORG_READ_PAT: ${{ secrets.ORG_READ_PAT }}
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
// ── ORG MEMBER BYPASS ────────────────────────────────────────────────────────
// GITHUB_TOKEN authenticates as github-actions[bot], which is not an org member.
// The checkMembershipForUser endpoint therefore returns 302 for everyone when
// using GITHUB_TOKEN — it cannot distinguish members from non-members.
//
// Solution: use ORG_READ_PAT (a classic PAT with read:org scope stored as a
// repo/org secret) only for this call. All write operations below still use
// the default GITHUB_TOKEN so automated comments appear as github-actions[bot].
//
// Fallback: if ORG_READ_PAT is not configured, the GITHUB_TOKEN is used and
// only *public* org members are bypassed (302 is treated as non-member).
const orgReadToken = process.env.ORG_READ_PAT;
const orgClient = orgReadToken
? require('@actions/github').getOctokit(orgReadToken)
: github;
try {
const { status } = await orgClient.rest.orgs.checkMembershipForUser({
org: context.repo.owner,
username: pr.user.login,
});
if (status === 204) {
console.log(`@${pr.user.login} is a ${context.repo.owner} org member — skipping triage.`);
return;
}
} catch (e) {
if (e.status !== 404 && e.status !== 302) throw e;
// 404 = confirmed non-member; 302 = token lacks org read → treat as non-member
}
// ── PART A: detect non-project practice / test submissions ────────────────
// Only scan title and branch — the PR template body contains the word
// "bootcamp" itself and would cause false positives if included.
const title = pr.title || '';
const branch = pr.head.ref || '';
const combined = `${title} ${branch}`.toLowerCase();
const nonProjectPatterns = [
/\bspc-\d+/i, // SPC-001, SPC-003-T1 …
/add\s+(jenkins|gitlab|github\s+actions)\s+(ci|pipeline)/i,
/add\s+ci\s+(workflow|pipeline)/i,
/push\s+(image|docker)\s+to\s+(ecr|dockerhub|registry)/i,
/add\s+(kubernetes|k8s)\s+manifests?/i,
/add\s+monitoring\s+(stack|dashboard)/i,
/\bdevops\s+(project|assignment|lab|homework|tp)\b/i,
/\b(lab|tp|homework|assignment|exercise)\s*[#\d]/i,
/\bbootcamp\b/i,
];
const matched = nonProjectPatterns.find((pattern) => pattern.test(combined));
if (matched) {
console.log(`Non-project pattern matched: ${matched} — closing PR #${pr.number}.`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['invalid'],
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: [
`## 👋 Hi @${pr.user.login},`,
'',
'It looks like this Pull Request may be a **practice, test, or course-related submission** rather than a contribution intended for the upstream project.',
'',
'This repository is widely used for learning and experimentation, but PRs opened only to practice on the sample are **out of scope** for the upstream project and will be closed.',
'',
'If you are testing ideas or learning on this sample, please keep those changes in **your own fork**.',
'',
`Please read our [CONTRIBUTING guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${context.payload.repository.default_branch}/CONTRIBUTING.md) for details on what kinds of contributions we accept.`,
'',
'_This is an automated message. If you believe this was a mistake, please re-open and leave a comment explaining why your PR is intended for the upstream project._',
].join('\n'),
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed',
});
// Stop here — no need to check the template on a non-project PR.
return;
}
console.log(`No non-project pattern detected on PR #${pr.number} — checking template completeness.`);
// ── PART B: check that the PR template was properly filled in ────────────
const body = pr.body || '';
function isIncomplete(text) {
if (text.trim().length < 30) return true;
// Template HTML comment still present → body was never edited
if (text.includes('<!-- Please describe your change')) return true;
// Placeholder issue reference was not replaced
if (/Fixes\s*#\s*\(issue\)/.test(text)) return true;
// All checklist items still unchecked
const unchecked = (text.match(/- \[ \]/g) || []).length;
const checked = (text.match(/- \[x\]/gi) || []).length;
if (unchecked >= 3 && checked === 0) return true;
return false;
}
const currentLabels = pr.labels.map(l => l.name);
const alreadyFlagged = currentLabels.includes('needs-information');
if (isIncomplete(body)) {
if (!alreadyFlagged) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['needs-information'],
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: [
`## 📋 Please complete the PR template, @${pr.user.login}`,
'',
'It looks like the Pull Request description is **empty or hasn\'t been filled in** yet.',
'',
'To help maintainers review your contribution, please:',
'',
'1. Edit this PR and fill in the description template:',
' - Describe **what** your change does and **why**',
' - Link the related issue (e.g. `Fixes #123`)',
' - Tick the checklist items that apply',
'',
'2. Once the template is complete, the `needs-information` label will be removed automatically.',
'',
'> **Note:** If this PR is not updated within **7 days**, it will be closed automatically. You are welcome to re-open it once the description is complete.',
'',
'_This is an automated message._',
].join('\n'),
});
console.log(`PR #${pr.number} flagged as needs-information.`);
} else {
console.log(`PR #${pr.number} still incomplete, already flagged.`);
}
} else {
if (alreadyFlagged) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name: 'needs-information',
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `Thanks for completing the PR template, @${pr.user.login}! The \`needs-information\` label has been removed. A maintainer will review your PR shortly. 🙏`,
});
console.log(`PR #${pr.number} template now complete, label removed.`);
} else {
console.log(`PR #${pr.number} template looks complete.`);
}
}
# ── 2. Daily: close PRs still labelled needs-information after 7 days ────────
close-stale-incomplete:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Close PRs with incomplete template after 7 days of inactivity
uses: actions/github-script@v7
with:
script: |
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const prs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
});
for (const pr of prs) {
const hasLabel = pr.labels.some(l => l.name === 'needs-information');
if (!hasLabel) continue;
const lastUpdate = new Date(pr.updated_at);
if (lastUpdate >= sevenDaysAgo) {
console.log(`PR #${pr.number} flagged but still recent (${pr.updated_at}), skipping.`);
continue;
}
console.log(`Closing PR #${pr.number} — no activity for 7+ days with incomplete template.`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: [
'## PR closed — template not completed',
'',
`Hi @${pr.user.login},`,
'',
'This Pull Request has been **automatically closed** because the description template was not completed within 7 days.',
'',
'You are welcome to **re-open it** once the template is fully filled in. A complete description helps maintainers understand the context and intent of your change.',
'',
`See our [CONTRIBUTING guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/CONTRIBUTING.md) for details.`,
'',
'_This is an automated message._',
].join('\n'),
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed',
});
}