-
Notifications
You must be signed in to change notification settings - Fork 2
97 lines (84 loc) · 3.66 KB
/
Copy pathpr-compliance.yml
File metadata and controls
97 lines (84 loc) · 3.66 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
name: PR Compliance
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
issues: read
jobs:
compliance:
name: Enforce PR template and issue-first policy
if: github.event.pull_request.user.login != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Validate PR body and issue linkage
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const body = pr.body || "";
const errors = [];
const requiredHeadings = [
"## Linked Issue (Required)",
"## Problem and User Value (Required)",
"## Solution Summary (Required)",
"## Verification (Required)",
"## Scope Declaration (Required)",
"## Contributor Acknowledgements (Required)"
];
for (const heading of requiredHeadings) {
if (!body.includes(heading)) {
errors.push(`Missing required section: ${heading}`);
}
}
const requiredChecks = [
"I linked an approved issue for this PR.",
"I ran relevant tests/lint and reported results above.",
"I reviewed every changed line and can explain it.",
"I read and agree to follow `CONTRIBUTING.md`.",
"I read and agree to follow `CODE_OF_CONDUCT.md`."
];
for (const item of requiredChecks) {
const re = new RegExp(`- \\[[xX]\\] ${item.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}`);
if (!re.test(body)) {
errors.push(`Unchecked acknowledgement: "${item}"`);
}
}
const issueMatches = [...body.matchAll(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)|#(\d+)/gi)];
const issueNumbers = [...new Set(issueMatches.map((m) => Number(m[1] || m[2])).filter(Number.isInteger))];
if (issueNumbers.length === 0) {
errors.push("No linked issue found. Use 'Closes #<issue-number>' in the Linked Issue section.");
}
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const isMaintainer = maintainerAssociations.has(pr.author_association);
if (!isMaintainer && issueNumbers.length > 0) {
let hasApprovedIssue = false;
for (const issue_number of issueNumbers) {
try {
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number
});
if (issue.data.pull_request) {
continue;
}
const labels = issue.data.labels.map((l) => (typeof l === "string" ? l : l.name));
if (labels.includes("ready-for-pr") || labels.includes("accepted")) {
hasApprovedIssue = true;
break;
}
} catch (error) {
errors.push(`Unable to validate linked issue #${issue_number}: ${error.message}`);
}
}
if (!hasApprovedIssue) {
errors.push("Linked issue must be maintainer-approved with label 'ready-for-pr' or 'accepted'.");
}
}
if (errors.length > 0) {
core.setFailed(["PR compliance check failed:", ...errors.map((e) => `- ${e}`)].join("\n"));
} else {
core.info("PR compliance check passed.");
}