Skip to content

Commit 3427348

Browse files
committed
Auto-close duplicate effort PRs
1 parent a5d04d6 commit 3427348

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Close Duplicate Effort PRs
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
close-if-issue-assigned:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 5
11+
12+
steps:
13+
- name: Check org membership and referenced issues
14+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
15+
with:
16+
script: |
17+
const prAuthor = context.payload.pull_request.user.login;
18+
const prNumber = context.payload.pull_request.number;
19+
const owner = context.repo.owner;
20+
const repo = context.repo.repo;
21+
22+
// Check if PR author is a member of the getsentry org
23+
try {
24+
await github.rest.orgs.checkMembershipForUser({
25+
org: 'getsentry',
26+
username: prAuthor,
27+
});
28+
console.log(`${prAuthor} is a member of getsentry, skipping.`);
29+
return;
30+
} catch (error) {
31+
// 404 means not a member, which is what we want to continue with
32+
if (error.status !== 404) {
33+
throw error;
34+
}
35+
console.log(`${prAuthor} is not a member of getsentry.`);
36+
}
37+
38+
// Extract issue references from PR title and body
39+
const prTitle = context.payload.pull_request.title || '';
40+
const prBody = context.payload.pull_request.body || '';
41+
const text = `${prTitle}\n${prBody}`;
42+
43+
// Match patterns like #123, fixes #123, closes #123, resolves #123,
44+
// as well as full URL references like github.com/owner/repo/issues/123
45+
const issuePattern = /(?:(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s*:?\s*)?(?:https?:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/(\d+)|#(\d+))/gi;
46+
47+
const issueNumbers = new Set();
48+
let match;
49+
while ((match = issuePattern.exec(text)) !== null) {
50+
const num = match[1] || match[2];
51+
issueNumbers.add(parseInt(num, 10));
52+
}
53+
54+
if (issueNumbers.size === 0) {
55+
console.log('No issue references found in PR, skipping.');
56+
return;
57+
}
58+
59+
console.log(`Found referenced issues: ${[...issueNumbers].join(', ')}`);
60+
61+
// Check if any referenced issue has an assignee
62+
for (const issueNumber of issueNumbers) {
63+
let issue;
64+
try {
65+
const response = await github.rest.issues.get({
66+
owner,
67+
repo,
68+
issue_number: issueNumber,
69+
});
70+
issue = response.data;
71+
} catch (error) {
72+
console.log(`Could not fetch issue #${issueNumber}: ${error.message}`);
73+
continue;
74+
}
75+
76+
if (issue.assignees && issue.assignees.length > 0) {
77+
const assignees = issue.assignees.map(a => a.login).join(', ');
78+
console.log(`Issue #${issueNumber} is assigned to: ${assignees}. Closing PR.`);
79+
80+
// Add a comment explaining why the PR is being closed
81+
await github.rest.issues.createComment({
82+
owner,
83+
repo,
84+
issue_number: prNumber,
85+
body: `Hey @${prAuthor}, thanks for contributing!\n\nIt looks like the issue (#${issueNumber}) you're addressing in this pull request already has someone assigned to it. Please check on the issue whether help is wanted before opening a pull request.\n\nIf you believe your contribution is still valuable, please leave a comment on the issue to coordinate with the assignee.`,
86+
});
87+
88+
// Close the PR
89+
await github.rest.pulls.update({
90+
owner,
91+
repo,
92+
pull_number: prNumber,
93+
state: 'closed',
94+
});
95+
96+
return;
97+
}
98+
}
99+
100+
console.log('No referenced issues have assignees, PR is fine.');

0 commit comments

Comments
 (0)