-
Notifications
You must be signed in to change notification settings - Fork 2
145 lines (122 loc) · 6.32 KB
/
Copy pathpr-validator.yml
File metadata and controls
145 lines (122 loc) · 6.32 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
name: PR Validator
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
pull-requests: write
issues: write
contents: read
jobs:
validate-pr:
name: validate-pr
runs-on: ubuntu-latest
steps:
- name: Validate PR and enforce contribution rules
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request;
const title = pr.title;
const body = pr.body || "";
const headRef = pr.head.ref;
const baseRef = pr.base.ref;
const author = pr.user.login;
const prNumber = pr.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const failures = [];
// ── Check 1: PR title format ─────────────────────────────────────────
const titleRegex = /^(feat|fix|docs|chore|refactor|test|style|perf)\((app|backend|ml-worker|repo)\): .{10,}$/;
if (!titleRegex.test(title)) {
failures.push(
`**Invalid PR title:** \`${title}\` — must follow \`type(scope): description\` format where type is one of \`feat|fix|docs|chore|refactor|test|style|perf\`, scope is one of \`app|backend|ml-worker|repo\`, and description is at least 10 characters.`
);
}
// ── Check 2: PR body sections ────────────────────────────────────────
const hasWhatChanged = /##\s*what changed/i.test(body);
const hasWhy = /##\s*why/i.test(body);
const hasHowToTest = /##\s*how to test/i.test(body);
if (!body.trim() || !hasWhatChanged || !hasWhy || !hasHowToTest) {
failures.push(
`**Missing PR description sections:** Your PR body must include all three headings: \`## What changed\`, \`## Why\`, and \`## How to test\`. Add them and fill them in.`
);
}
// ── Check 3: Linked issue ────────────────────────────────────────────
const issueRegex = /(?:closes|fixes|resolves|relates to|ref)\s+#\d+/i;
if (!issueRegex.test(body)) {
failures.push(
`**No linked issue:** Reference the issue this PR addresses using \`closes #N\`, \`fixes #N\`, or \`relates to #N\`. Open an issue first if one doesn't exist.`
);
}
// ── Check 4: Target branch ───────────────────────────────────────────
if (baseRef !== "main") {
failures.push(
`**Wrong target branch:** This PR targets \`${baseRef}\` but all PRs must target \`main\`.`
);
}
// ── Check 5: Branch name ─────────────────────────────────────────────
const branchRegex = /^(feat|fix|docs|chore|refactor|test|style|perf)\/(app|backend|ml-worker|repo|shared)\/[a-z0-9-]+$/;
if (!branchRegex.test(headRef)) {
failures.push(
`**Invalid branch name:** \`${headRef}\` — must follow \`type/scope/short-description\` e.g. \`feat/app/timetable-progress\`.`
);
}
// ── Helper: safe label operations ───────────────────────────────────
async function addLabel(name) {
try {
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber,
labels: [name],
});
} catch (e) {
// Label may not exist in the repo yet — skip silently
core.warning(`Could not add label "${name}": ${e.message}`);
}
}
async function removeLabel(name) {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber,
name,
});
} catch (e) {
// Label not present — ignore 404
}
}
// ── All checks passed ────────────────────────────────────────────────
if (failures.length === 0) {
await addLabel("ready-for-review");
await removeLabel("invalid-pr");
console.log("✅ PR passed all validation checks.");
return;
}
// ── Checks failed — build comment ────────────────────────────────────
const bulletList = failures.map(f => `- ${f}`).join("\n");
const comment = `## ❌ PR Validation Failed
Hi @${author}, this PR was automatically closed because it does not follow Sentri's contribution guidelines. Please read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md) before re-submitting.
### Issues found:
${bulletList}
### How to fix and reopen
1. Fix all issues listed above
2. Push your changes or edit the PR title/description
3. Reopen the PR — validation will run again automatically
---
_This check is automated. If you believe this is a mistake, ping a maintainer._`;
// Post comment
await github.rest.issues.createComment({
owner, repo,
issue_number: prNumber,
body: comment,
});
// Apply / remove labels
await addLabel("invalid-pr");
await removeLabel("ready-for-review");
// Close the PR
await github.rest.pulls.update({
owner, repo,
pull_number: prNumber,
state: "closed",
});
// Fail the job so the status check is red
core.setFailed(`PR validation failed with ${failures.length} issue(s).`);