Skip to content

Commit 909833d

Browse files
committed
feat: update PR issue check to automatically close invalid PRs and support manual triggers via workflow_dispatch
1 parent 3187139 commit 909833d

1 file changed

Lines changed: 130 additions & 82 deletions

File tree

.github/workflows/pr-issue-check.yml

Lines changed: 130 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ name: PR Issue Link & Assignment Check
33
on:
44
pull_request_target:
55
types: [opened, edited, reopened, synchronize]
6+
workflow_dispatch:
7+
inputs:
8+
pr_number:
9+
description: 'PR Number to check (optional. If empty, checks all open PRs)'
10+
required: false
11+
type: string
612

713
permissions:
814
issues: write
@@ -20,111 +26,152 @@ jobs:
2026
github-token: ${{ secrets.GITHUB_TOKEN }}
2127
script: |
2228
const { owner, repo } = context.repo;
23-
const pr = context.payload.pull_request;
24-
const prNumber = pr.number;
25-
const prAuthor = pr.user.login;
26-
const prBody = pr.body ?? '';
27-
28-
// ----------------------------------------------------------------
29-
// 1. Parse a linked issue number from the PR body.
30-
// Matches: closes/fixes/resolves #123 (case-insensitive)
31-
// ----------------------------------------------------------------
32-
const LINK_REGEX =
33-
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
34-
35-
const issueNumbers = [];
36-
let match;
37-
while ((match = LINK_REGEX.exec(prBody)) !== null) {
38-
issueNumbers.push(parseInt(match[1], 10));
29+
let pullRequests = [];
30+
31+
const prInput = context.payload.inputs?.pr_number;
32+
if (prInput) {
33+
try {
34+
const { data: pr } = await github.rest.pulls.get({
35+
owner, repo, pull_number: parseInt(prInput, 10),
36+
});
37+
pullRequests.push(pr);
38+
} catch (err) {
39+
core.setFailed(`Failed to fetch PR #${prInput}: ${err.message}`);
40+
return;
41+
}
42+
} else if (context.eventName === 'workflow_dispatch') {
43+
const { data: prs } = await github.rest.pulls.list({
44+
owner, repo, state: 'open', per_page: 100,
45+
});
46+
pullRequests = prs;
47+
core.info(`Fetched ${prs.length} open PR(s) to verify.`);
48+
} else {
49+
pullRequests.push(context.payload.pull_request);
3950
}
4051
4152
const SENTINEL_NO_LINK = '<!-- pr-issue-check: no-link -->';
4253
const SENTINEL_NOT_ASSIGNED = '<!-- pr-issue-check: not-assigned -->';
4354
4455
// Helper: fetch existing bot comments on this PR
45-
async function getBotComments() {
56+
async function getBotComments(number) {
4657
const { data: comments } = await github.rest.issues.listComments({
47-
owner, repo, issue_number: prNumber, per_page: 100,
58+
owner, repo, issue_number: number, per_page: 100,
4859
});
4960
return comments.filter(c => c.user?.login === 'github-actions[bot]');
5061
}
5162
5263
// Helper: check whether we already posted a specific sentinel
53-
async function alreadyPosted(sentinel) {
54-
const comments = await getBotComments();
64+
async function alreadyPosted(number, sentinel) {
65+
const comments = await getBotComments(number);
5566
return comments.some(c => c.body?.includes(sentinel));
5667
}
5768
58-
// ----------------------------------------------------------------
59-
// 2. NO linked issue found → warn and stop (do NOT close the PR)
60-
// ----------------------------------------------------------------
61-
if (issueNumbers.length === 0) {
62-
const already = await alreadyPosted(SENTINEL_NO_LINK);
63-
if (!already) {
64-
await github.rest.issues.createComment({
65-
owner, repo, issue_number: prNumber,
66-
body: `👋 Hey @${prAuthor}! Thanks for your contribution! 🎉
69+
for (const pr of pullRequests) {
70+
const prNumber = pr.number;
71+
const prAuthor = pr.user.login;
72+
const prBody = pr.body ?? '';
6773
68-
${SENTINEL_NO_LINK}
69-
It looks like this PR isn't linked to any issue yet.
74+
core.info(`Checking PR #${prNumber} by @${prAuthor}...`);
75+
76+
// ----------------------------------------------------------------
77+
// 1. Parse a linked issue number from the PR body.
78+
// Matches: closes/fixes/resolves #123 (case-insensitive)
79+
// ----------------------------------------------------------------
80+
const LINK_REGEX =
81+
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
82+
83+
const issueNumbers = [];
84+
let match;
85+
while ((match = LINK_REGEX.exec(prBody)) !== null) {
86+
issueNumbers.push(parseInt(match[1], 10));
87+
}
88+
89+
// ----------------------------------------------------------------
90+
// 2. NO linked issue found → warn, close the PR, and stop
91+
// ----------------------------------------------------------------
92+
if (issueNumbers.length === 0) {
93+
const already = await alreadyPosted(prNumber, SENTINEL_NO_LINK);
94+
if (!already) {
95+
await github.rest.issues.createComment({
96+
owner, repo, issue_number: prNumber,
97+
body: `👋 Hey @${prAuthor}! Thanks for your contribution! 🎉
7098
71-
Please edit your PR description and add a closing keyword so we can track this properly, for example:
99+
${SENTINEL_NO_LINK}
100+
Unfortunately, this PR has been **automatically closed** because it is not linked to any open issue.
72101
73-
\`\`\`
74-
Fixes #<issue-number>
75-
\`\`\`
102+
To resolve this, please do the following:
103+
1. **Link a valid open issue** by editing your PR description to include a closing keyword (e.g., \`Fixes #<issue-number>\`).
104+
2. **Reopen this PR** once the link is added.
76105
77106
> 💡 You can link multiple issues if needed (e.g. \`Fixes #12, Closes #34\`).
78107
> 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.
79108
80-
Once you've updated the PR description the check will re-run automatically. 🙌`,
109+
We look forward to reviewing your PR once an issue is linked! 🚀`,
110+
});
111+
}
112+
113+
// Close the PR
114+
await github.rest.pulls.update({
115+
owner, repo, pull_number: prNumber, state: 'closed',
81116
});
117+
118+
continue; // check next PR
82119
}
83-
return; // stop here — don't close, just inform
84-
}
85120
86-
// ----------------------------------------------------------------
87-
// 3. Linked issue(s) found — check assignment for each one.
88-
// We validate all linked issues and act on the first violation.
89-
// ----------------------------------------------------------------
90-
for (const issueNumber of issueNumbers) {
91-
let issue;
92-
try {
93-
const { data } = await github.rest.issues.get({
94-
owner, repo, issue_number: issueNumber,
95-
});
96-
issue = data;
97-
} catch (err) {
98-
if (err.status === 404) {
99-
// Linked issue doesn't exist — treat same as no-link
100-
const already = await alreadyPosted(SENTINEL_NO_LINK);
101-
if (!already) {
102-
await github.rest.issues.createComment({
103-
owner, repo, issue_number: prNumber,
104-
body: `👋 Hey @${prAuthor}!
121+
// ----------------------------------------------------------------
122+
// 3. Linked issue(s) found — check assignment for each one.
123+
// We validate all linked issues and act on the first violation.
124+
// ----------------------------------------------------------------
125+
let prViolation = false;
126+
for (const issueNumber of issueNumbers) {
127+
let issue;
128+
try {
129+
const { data } = await github.rest.issues.get({
130+
owner, repo, issue_number: issueNumber,
131+
});
132+
issue = data;
133+
} catch (err) {
134+
if (err.status === 404) {
135+
// Linked issue doesn't exist — treat same as no-link
136+
const already = await alreadyPosted(prNumber, SENTINEL_NO_LINK);
137+
if (!already) {
138+
await github.rest.issues.createComment({
139+
owner, repo, issue_number: prNumber,
140+
body: `👋 Hey @${prAuthor}!
105141
106142
${SENTINEL_NO_LINK}
107-
This PR references issue **#${issueNumber}**, but that issue doesn't seem to exist (or was deleted).
143+
Unfortunately, this PR has been **automatically closed** because the referenced issue **#${issueNumber}** does not exist (or was deleted).
144+
145+
To resolve this, please do the following:
146+
1. **Link a valid open issue** by editing your PR description to include a closing keyword (e.g., \`Fixes #<issue-number>\`).
147+
2. **Reopen this PR** once the link is added.
108148
109149
Please link a valid open issue before this PR can be reviewed. 🙏`,
150+
});
151+
}
152+
153+
// Close the PR
154+
await github.rest.pulls.update({
155+
owner, repo, pull_number: prNumber, state: 'closed',
110156
});
157+
158+
prViolation = true;
159+
break;
111160
}
112-
return;
161+
throw err;
113162
}
114-
throw err;
115-
}
116163
117-
// Is the PR author assigned to this issue?
118-
const assignees = issue.assignees.map(a => a.login.toLowerCase());
119-
const isAssigned = assignees.includes(prAuthor.toLowerCase());
164+
// Is the PR author assigned to this issue?
165+
const assignees = issue.assignees.map(a => a.login.toLowerCase());
166+
const isAssigned = assignees.includes(prAuthor.toLowerCase());
120167
121-
if (!isAssigned) {
122-
// Close the PR with a polite explanation
123-
const already = await alreadyPosted(SENTINEL_NOT_ASSIGNED);
124-
if (!already) {
125-
await github.rest.issues.createComment({
126-
owner, repo, issue_number: prNumber,
127-
body: `👋 Hey @${prAuthor}! Thanks for your interest in contributing to CommitPulse! 🙏
168+
if (!isAssigned) {
169+
// Close the PR with a polite explanation
170+
const already = await alreadyPosted(prNumber, SENTINEL_NOT_ASSIGNED);
171+
if (!already) {
172+
await github.rest.issues.createComment({
173+
owner, repo, issue_number: prNumber,
174+
body: `👋 Hey @${prAuthor}! Thanks for your interest in contributing to CommitPulse! 🙏
128175
129176
${SENTINEL_NOT_ASSIGNED}
130177
Unfortunately, this PR has been **automatically closed** because you are not assigned to the linked issue **[#${issueNumber} — ${issue.title}](${issue.html_url})**.
@@ -138,19 +185,20 @@ jobs:
138185
> 💡 You can be assigned to up to **3** open issues at a time. Check your current assignments before claiming a new one.
139186
140187
We look forward to your contribution once you're assigned! 🚀`,
188+
});
189+
}
190+
191+
// Close the PR
192+
await github.rest.pulls.update({
193+
owner, repo, pull_number: prNumber, state: 'closed',
141194
});
142-
}
143195
144-
// Close the PR
145-
await github.rest.pulls.update({
146-
owner, repo, pull_number: prNumber, state: 'closed',
147-
});
196+
prViolation = true;
197+
break; // stop after first violation
198+
}
199+
}
148200
149-
return; // stop after first violation
201+
if (!prViolation) {
202+
core.info(`✅ PR #${prNumber} by @${prAuthor} is properly linked and assigned.`);
150203
}
151204
}
152-
153-
// ----------------------------------------------------------------
154-
// 4. All checks passed — nothing to do.
155-
// ----------------------------------------------------------------
156-
core.info(`✅ PR #${prNumber} by @${prAuthor} is properly linked and assigned.`);

0 commit comments

Comments
 (0)