Skip to content

Commit a34efd6

Browse files
committed
[feature/releaser] Automate assigning/unassigning issues #571
I've created four GitHub Actions workflow files to automate issue assignment/unassignment. Created Workflows: 1. issue-assignment.yml – Auto-response to 'assign me' requests 2. pr-issue-link.yml – Auto-assign on PR 3. stale-pr-management.yml – Stale PR management (tracks contributor inactivity only) 4. pr-reopen-reassign.yml – Handle contributor follow-up Fixes #571
1 parent f3fc7c2 commit a34efd6

File tree

4 files changed

+546
-0
lines changed

4 files changed

+546
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Issue Assignment Bot
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
issues: write
9+
10+
jobs:
11+
respond-to-assign-request:
12+
runs-on: ubuntu-latest
13+
if: |
14+
github.event.issue.pull_request == null &&
15+
(
16+
contains(toLower(github.event.comment.body), 'assign this issue to me') ||
17+
contains(toLower(github.event.comment.body), 'assign me') ||
18+
contains(toLower(github.event.comment.body), 'can i work on this') ||
19+
contains(toLower(github.event.comment.body), 'i would like to work on this') ||
20+
contains(toLower(github.event.comment.body), 'i want to work on this')
21+
)
22+
steps:
23+
- name: Respond to assignment request
24+
uses: actions/github-script@v7
25+
with:
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
script: |
28+
const contributingUrl = 'https://openwisp.io/docs/developer/contributing.html';
29+
const commenter = context.payload.comment.user.login;
30+
31+
const message = `Hi @${commenter} 👋,
32+
33+
Thank you for your interest in contributing to OpenWISP! 🎉
34+
35+
According to our [contributing guidelines](${contributingUrl}), **you don't need to wait to be assigned** to start working on an issue. We encourage you to:
36+
37+
1. **Fork the repository** and start working on your solution
38+
2. **Open a Pull Request (PR) as soon as possible** - even as a draft if it's still in progress
39+
3. **Link your PR to this issue** by including \`Fixes #${context.payload.issue.number}\` in the PR description
40+
41+
Once you open a PR that references this issue, you will be automatically assigned to it.
42+
43+
This approach helps us:
44+
- See your progress and provide early feedback
45+
- Avoid multiple contributors working on the same issue unknowingly
46+
- Keep the contribution process moving smoothly
47+
48+
We look forward to your contribution! If you have any questions, feel free to ask in the PR or check our [documentation](${contributingUrl}).
49+
50+
Happy coding! 🚀`;
51+
52+
await github.rest.issues.createComment({
53+
owner: context.repo.owner,
54+
repo: context.repo.repo,
55+
issue_number: context.payload.issue.number,
56+
body: message
57+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: PR Issue Auto-Assignment
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened]
6+
7+
permissions:
8+
issues: write
9+
pull-requests: read
10+
11+
jobs:
12+
auto-assign-issue:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-assign linked issues to PR author
16+
uses: actions/github-script@v7
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
script: |
20+
const prBody = context.payload.pull_request.body || '';
21+
const prAuthor = context.payload.pull_request.user.login;
22+
const prNumber = context.payload.pull_request.number;
23+
const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi;
24+
const matches = [...prBody.matchAll(issuePattern)];
25+
if (matches.length === 0) {
26+
console.log('No linked issues found in PR body');
27+
return;
28+
}
29+
const MAX_ISSUES_TO_PROCESS = 10;
30+
if (matches.length > MAX_ISSUES_TO_PROCESS) {
31+
console.log(`Found ${matches.length} issue references, processing first ${MAX_ISSUES_TO_PROCESS} to avoid rate limits`);
32+
}
33+
const processedIssues = new Set();
34+
for (const match of matches) {
35+
const issueNumber = parseInt(match[1], 10);
36+
if (processedIssues.has(issueNumber)) {
37+
continue;
38+
}
39+
if (processedIssues.size >= MAX_ISSUES_TO_PROCESS) {
40+
console.log(`Reached limit of ${MAX_ISSUES_TO_PROCESS} issues, stopping processing`);
41+
break;
42+
}
43+
44+
processedIssues.add(issueNumber);
45+
try {
46+
const issue = await github.rest.issues.get({
47+
owner: context.repo.owner,
48+
repo: context.repo.repo,
49+
issue_number: issueNumber
50+
});
51+
if (issue.data.pull_request) {
52+
console.log(`#${issueNumber} is a pull request, skipping`);
53+
continue;
54+
}
55+
if (issue.data.assignees && issue.data.assignees.length > 0) {
56+
const currentAssignees = issue.data.assignees.map(a => a.login);
57+
if (currentAssignees.includes(prAuthor)) {
58+
console.log(`Issue #${issueNumber} already assigned to ${prAuthor}`);
59+
continue;
60+
}
61+
console.log(`Issue #${issueNumber} already assigned to: ${currentAssignees.join(', ')}`);
62+
continue;
63+
}
64+
await github.rest.issues.addAssignees({
65+
owner: context.repo.owner,
66+
repo: context.repo.repo,
67+
issue_number: issueNumber,
68+
assignees: [prAuthor]
69+
});
70+
71+
console.log(`Assigned issue #${issueNumber} to ${prAuthor}`);
72+
await github.rest.issues.createComment({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
issue_number: issueNumber,
76+
body: `This issue has been automatically assigned to @${prAuthor} who opened PR #${prNumber} to address it. 🎯`
77+
});
78+
79+
} catch (error) {
80+
if (error.status === 404) {
81+
console.log(`Issue #${issueNumber} not found`);
82+
} else {
83+
console.error(`Error processing issue #${issueNumber}:`, error.message);
84+
}
85+
}
86+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: PR Reopen Reassignment
2+
3+
on:
4+
pull_request_target:
5+
types: [reopened]
6+
issue_comment:
7+
types: [created]
8+
9+
permissions:
10+
issues: write
11+
pull-requests: read
12+
13+
jobs:
14+
reassign-on-reopen:
15+
runs-on: ubuntu-latest
16+
if: github.event_name == 'pull_request_target' && github.event.action == 'reopened'
17+
steps:
18+
- name: Reassign issues on PR reopen
19+
uses: actions/github-script@v7
20+
with:
21+
github-token: ${{ secrets.GITHUB_TOKEN }}
22+
script: |
23+
const prBody = context.payload.pull_request.body || '';
24+
const prAuthor = context.payload.pull_request.user.login;
25+
const prNumber = context.payload.pull_request.number;
26+
const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi;
27+
const matches = [...prBody.matchAll(issuePattern)];
28+
if (matches.length === 0) {
29+
console.log('No linked issues found in PR body');
30+
return;
31+
}
32+
const processedIssues = new Set();
33+
for (const match of matches) {
34+
const issueNumber = parseInt(match[1], 10);
35+
if (processedIssues.has(issueNumber)) {
36+
continue;
37+
}
38+
processedIssues.add(issueNumber);
39+
try {
40+
const issue = await github.rest.issues.get({
41+
owner: context.repo.owner,
42+
repo: context.repo.repo,
43+
issue_number: issueNumber
44+
});
45+
if (issue.data.pull_request) {
46+
continue;
47+
}
48+
const currentAssignees = issue.data.assignees?.map(a => a.login) || [];
49+
if (currentAssignees.length > 0 && !currentAssignees.includes(prAuthor)) {
50+
console.log(`Issue #${issueNumber} is assigned to others, skipping`);
51+
continue;
52+
}
53+
if (!currentAssignees.includes(prAuthor)) {
54+
await github.rest.issues.addAssignees({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
issue_number: issueNumber,
58+
assignees: [prAuthor]
59+
});
60+
console.log(`Reassigned issue #${issueNumber} to ${prAuthor}`);
61+
await github.rest.issues.createComment({
62+
owner: context.repo.owner,
63+
repo: context.repo.repo,
64+
issue_number: issueNumber,
65+
body: `Welcome back, @${prAuthor}! 🎉 This issue has been reassigned to you as you've reopened PR #${prNumber}.`
66+
});
67+
}
68+
} catch (error) {
69+
console.error(`Error processing issue #${issueNumber}:`, error.message);
70+
}
71+
}
72+
try {
73+
await github.rest.issues.removeLabel({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
issue_number: prNumber,
77+
name: 'stale'
78+
});
79+
} catch (e) {
80+
81+
}
82+
83+
handle-contributor-activity:
84+
runs-on: ubuntu-latest
85+
if: |
86+
github.event_name == 'issue_comment' &&
87+
github.event.issue.pull_request != null
88+
steps:
89+
- name: Check and handle stale PR activity
90+
uses: actions/github-script@v7
91+
with:
92+
github-token: ${{ secrets.GITHUB_TOKEN }}
93+
script: |
94+
const prNumber = context.payload.issue.number;
95+
const commenter = context.payload.comment.user.login;
96+
const { data: pr } = await github.rest.pulls.get({
97+
owner: context.repo.owner,
98+
repo: context.repo.repo,
99+
pull_number: prNumber
100+
});
101+
if (commenter !== pr.user.login) {
102+
console.log('Comment not from PR author, skipping');
103+
return;
104+
}
105+
const labels = context.payload.issue.labels?.map(l => l.name) || [];
106+
if (!labels.includes('stale')) {
107+
console.log('PR is not stale, skipping');
108+
return;
109+
}
110+
try {
111+
await github.rest.issues.removeLabel({
112+
owner: context.repo.owner,
113+
repo: context.repo.repo,
114+
issue_number: prNumber,
115+
name: 'stale'
116+
});
117+
console.log('Removed stale label');
118+
} catch (e) {
119+
console.log('Could not remove stale label:', e.message);
120+
}
121+
const prBody = pr.body || '';
122+
const issuePattern = /(?:fix(?:es)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi;
123+
const matches = [...prBody.matchAll(issuePattern)];
124+
for (const match of matches) {
125+
const issueNumber = parseInt(match[1], 10);
126+
try {
127+
const issue = await github.rest.issues.get({
128+
owner: context.repo.owner,
129+
repo: context.repo.repo,
130+
issue_number: issueNumber
131+
});
132+
if (issue.data.pull_request) {
133+
continue;
134+
}
135+
const currentAssignees = issue.data.assignees?.map(a => a.login) || [];
136+
if (currentAssignees.length === 0) {
137+
await github.rest.issues.addAssignees({
138+
owner: context.repo.owner,
139+
repo: context.repo.repo,
140+
issue_number: issueNumber,
141+
assignees: [commenter]
142+
});
143+
console.log(`Reassigned issue #${issueNumber} to ${commenter}`);
144+
}
145+
} catch (error) {
146+
console.error(`Error reassigning issue #${issueNumber}:`, error.message);
147+
}
148+
}
149+
await github.rest.issues.createComment({
150+
owner: context.repo.owner,
151+
repo: context.repo.repo,
152+
issue_number: prNumber,
153+
body: `Thanks for following up, @${commenter}! 🙌 The stale status has been removed and the linked issue(s) have been reassigned to you. Looking forward to your updates!`
154+
});

0 commit comments

Comments
 (0)