forked from JhaSourav07/commitpulse
-
Notifications
You must be signed in to change notification settings - Fork 0
204 lines (169 loc) · 8.76 KB
/
Copy pathpr-issue-check.yml
File metadata and controls
204 lines (169 loc) · 8.76 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
name: PR Issue Link & Assignment Check
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
workflow_dispatch:
inputs:
pr_number:
description: 'PR Number to check (optional. If empty, checks all open PRs)'
required: false
type: string
permissions:
issues: write
pull-requests: write
jobs:
check-issue-link:
name: Verify PR is Linked to an Assigned Issue
runs-on: ubuntu-latest
steps:
- name: Check linked issue and assignment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
let pullRequests = [];
const prInput = context.payload.inputs?.pr_number;
if (prInput) {
try {
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: parseInt(prInput, 10),
});
pullRequests.push(pr);
} catch (err) {
core.setFailed(`Failed to fetch PR #${prInput}: ${err.message}`);
return;
}
} else if (context.eventName === 'workflow_dispatch') {
const { data: prs } = await github.rest.pulls.list({
owner, repo, state: 'open', per_page: 100,
});
pullRequests = prs;
core.info(`Fetched ${prs.length} open PR(s) to verify.`);
} else {
pullRequests.push(context.payload.pull_request);
}
const SENTINEL_NO_LINK = '<!-- pr-issue-check: no-link -->';
const SENTINEL_NOT_ASSIGNED = '<!-- pr-issue-check: not-assigned -->';
// Helper: fetch existing bot comments on this PR
async function getBotComments(number) {
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: number, per_page: 100,
});
return comments.filter(c => c.user?.login === 'github-actions[bot]');
}
// Helper: check whether we already posted a specific sentinel
async function alreadyPosted(number, sentinel) {
const comments = await getBotComments(number);
return comments.some(c => c.body?.includes(sentinel));
}
for (const pr of pullRequests) {
const prNumber = pr.number;
const prAuthor = pr.user.login;
const prBody = pr.body ?? '';
core.info(`Checking PR #${prNumber} by @${prAuthor}...`);
// ----------------------------------------------------------------
// 1. Parse a linked issue number from the PR body.
// Matches: closes/fixes/resolves #123 (case-insensitive)
// ----------------------------------------------------------------
const LINK_REGEX =
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
const issueNumbers = [];
let match;
while ((match = LINK_REGEX.exec(prBody)) !== null) {
issueNumbers.push(parseInt(match[1], 10));
}
// ----------------------------------------------------------------
// 2. NO linked issue found → warn, close the PR, and stop
// ----------------------------------------------------------------
if (issueNumbers.length === 0) {
const already = await alreadyPosted(prNumber, SENTINEL_NO_LINK);
if (!already) {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: `👋 Hey @${prAuthor}! Thanks for your contribution! 🎉
${SENTINEL_NO_LINK}
Unfortunately, this PR has been **automatically closed** because it is not linked to any open issue.
To resolve this, please do the following:
1. **Link a valid open issue** by editing your PR description to include a closing keyword (e.g., \`Fixes #<issue-number>\`).
2. **Reopen this PR** once the link is added.
> 💡 You can link multiple issues if needed (e.g. \`Fixes #12, Closes #34\`).
> If you're working on something that doesn't have an issue yet, please [open one first](https://github.com/${owner}/${repo}/issues/new/choose) and then link it here.
We look forward to reviewing your PR once an issue is linked! 🚀`,
});
}
// Close the PR
await github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'closed',
});
continue; // check next PR
}
// ----------------------------------------------------------------
// 3. Linked issue(s) found — check assignment for each one.
// We validate all linked issues and act on the first violation.
// ----------------------------------------------------------------
let prViolation = false;
for (const issueNumber of issueNumbers) {
let issue;
try {
const { data } = await github.rest.issues.get({
owner, repo, issue_number: issueNumber,
});
issue = data;
} catch (err) {
if (err.status === 404) {
// Linked issue doesn't exist — treat same as no-link
const already = await alreadyPosted(prNumber, SENTINEL_NO_LINK);
if (!already) {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: `👋 Hey @${prAuthor}!
${SENTINEL_NO_LINK}
Unfortunately, this PR has been **automatically closed** because the referenced issue **#${issueNumber}** does not exist (or was deleted).
To resolve this, please do the following:
1. **Link a valid open issue** by editing your PR description to include a closing keyword (e.g., \`Fixes #<issue-number>\`).
2. **Reopen this PR** once the link is added.
Please link a valid open issue before this PR can be reviewed. 🙏`,
});
}
// Close the PR
await github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'closed',
});
prViolation = true;
break;
}
throw err;
}
// Is the PR author assigned to this issue?
const assignees = issue.assignees.map(a => a.login.toLowerCase());
const isAssigned = assignees.includes(prAuthor.toLowerCase());
if (!isAssigned) {
// Close the PR with a polite explanation
const already = await alreadyPosted(prNumber, SENTINEL_NOT_ASSIGNED);
if (!already) {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber,
body: `👋 Hey @${prAuthor}! Thanks for your interest in contributing to CommitPulse! 🙏
${SENTINEL_NOT_ASSIGNED}
Unfortunately, this PR has been **automatically closed** because you are not assigned to the linked issue **[#${issueNumber} — ${issue.title}](${issue.html_url})**.
To avoid this in the future, please follow these steps:
1. **Claim the issue** — Comment \`/claim\` on [#${issueNumber}](${issue.html_url}) if you are the issue author, or ask a maintainer to \`/assign\` you.
2. **Wait for confirmation** — The bot will confirm your assignment with a ✅ reply.
3. **Then open your PR** — Link the issue with \`Fixes #${issueNumber}\` in your description.
> 💡 You can be assigned to up to **5** open issues at a time. Check your current assignments before claiming a new one.
We look forward to your contribution once you're assigned! 🚀`,
});
}
// Close the PR
await github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'closed',
});
prViolation = true;
break; // stop after first violation
}
}
if (!prViolation) {
core.info(`✅ PR #${prNumber} by @${prAuthor} is properly linked and assigned.`);
}
}