Skip to content

Commit dd20387

Browse files
feat: add issue assignment bot workflow (#167)
1 parent abd6f7c commit dd20387

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
name: Issue Assignment Bot
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
issues: write
9+
contents: read
10+
11+
jobs:
12+
assign-issue:
13+
# Only run on issue comments (not PR comments) and when comment contains @bot assign
14+
if: |
15+
!github.event.issue.pull_request &&
16+
contains(github.event.comment.body, '@bot assign')
17+
runs-on: ubuntu-latest
18+
# Prevent race conditions - only one job per issue at a time
19+
concurrency:
20+
group: issue-assign-${{ github.event.issue.number }}
21+
cancel-in-progress: false
22+
23+
steps:
24+
- name: Check and Assign Issue
25+
uses: actions/github-script@v8
26+
with:
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
script: |
29+
const commenter = context.payload.comment.user.login;
30+
const issue = context.payload.issue;
31+
const repo = context.repo;
32+
33+
console.log(`Processing @bot assign from ${commenter} on issue #${issue.number}`);
34+
35+
// Validate exact command match
36+
const commentBody = context.payload.comment.body.trim();
37+
if (commentBody !== '@bot assign') {
38+
console.log(`Comment "${commentBody}" is not an exact match for "@bot assign", skipping`);
39+
return;
40+
}
41+
42+
// Re-fetch issue to get current state (prevents race conditions)
43+
const { data: currentIssue } = await github.rest.issues.get({
44+
...repo,
45+
issue_number: issue.number
46+
});
47+
48+
// Check if issue is already assigned
49+
if (currentIssue.assignees && currentIssue.assignees.length > 0) {
50+
const currentAssignee = currentIssue.assignees[0].login;
51+
52+
// Check if already assigned to the same commenter
53+
if (currentAssignee === commenter) {
54+
console.log(`Issue already assigned to commenter ${commenter}`);
55+
await github.rest.issues.createComment({
56+
...repo,
57+
issue_number: issue.number,
58+
body: `@${commenter} This issue is already assigned to you.`
59+
});
60+
return;
61+
}
62+
63+
console.log(`Issue already assigned to ${currentAssignee}`);
64+
await github.rest.issues.createComment({
65+
...repo,
66+
issue_number: issue.number,
67+
body: `@${commenter} This issue cannot be claimed, as someone else is already working on it.`
68+
});
69+
return;
70+
}
71+
72+
// Check if commenter has any other open assigned issues using Search API
73+
const { data: searchResult } = await github.rest.search.issuesAndPullRequests({
74+
q: `repo:${repo.owner}/${repo.repo} is:issue is:open assignee:${commenter}`
75+
});
76+
77+
// Filter out the current issue and PRs
78+
const otherAssignedIssues = searchResult.items.filter(i =>
79+
i.number !== issue.number && !i.pull_request
80+
);
81+
82+
if (otherAssignedIssues.length > 0) {
83+
console.log(`User ${commenter} already has ${otherAssignedIssues.length} assigned issues`);
84+
85+
await github.rest.issues.createComment({
86+
...repo,
87+
issue_number: issue.number,
88+
body: `Hey @${commenter}, you already have an open issue assigned to you.\n\n**Please complete or close your existing issue before claiming a new one.**`
89+
});
90+
return;
91+
}
92+
93+
// Assign the issue
94+
console.log(`Assigning issue #${issue.number} to ${commenter}`);
95+
96+
try {
97+
await github.rest.issues.addAssignees({
98+
...repo,
99+
issue_number: issue.number,
100+
assignees: [commenter]
101+
});
102+
} catch (error) {
103+
console.log(`Failed to assign issue: ${error.message}`);
104+
await github.rest.issues.createComment({
105+
...repo,
106+
issue_number: issue.number,
107+
body: `@${commenter} Failed to assign this issue. Please try again or contact a maintainer.`
108+
});
109+
return;
110+
}
111+
112+
// Add "in progress" label
113+
try {
114+
await github.rest.issues.addLabels({
115+
...repo,
116+
issue_number: issue.number,
117+
labels: ['in progress']
118+
});
119+
console.log(`Added "in progress" label to issue #${issue.number}`);
120+
} catch (error) {
121+
console.log(`Could not add label: ${error.message}`);
122+
}
123+
124+
await github.rest.issues.createComment({
125+
...repo,
126+
issue_number: issue.number,
127+
body: `Assigned to @${commenter}!`
128+
});
129+
130+
console.log(`Successfully assigned issue #${issue.number} to ${commenter}`);
131+
132+
unclaim-issue:
133+
# Only run on issue comments (not PR comments) and when comment contains @bot unclaim
134+
if: |
135+
!github.event.issue.pull_request &&
136+
contains(github.event.comment.body, '@bot unclaim')
137+
runs-on: ubuntu-latest
138+
# Prevent race conditions - only one job per issue at a time
139+
concurrency:
140+
group: issue-assign-${{ github.event.issue.number }}
141+
cancel-in-progress: false
142+
143+
steps:
144+
- name: Unclaim Issue
145+
uses: actions/github-script@v8
146+
with:
147+
github-token: ${{ secrets.GITHUB_TOKEN }}
148+
script: |
149+
const commenter = context.payload.comment.user.login;
150+
const issue = context.payload.issue;
151+
const repo = context.repo;
152+
153+
console.log(`Processing @bot unclaim from ${commenter} on issue #${issue.number}`);
154+
155+
// Validate exact command match
156+
const commentBody = context.payload.comment.body.trim();
157+
if (commentBody !== '@bot unclaim') {
158+
console.log(`Comment "${commentBody}" is not an exact match for "@bot unclaim", skipping`);
159+
return;
160+
}
161+
162+
// Re-fetch issue to get current state
163+
const { data: currentIssue } = await github.rest.issues.get({
164+
...repo,
165+
issue_number: issue.number
166+
});
167+
168+
// Check if commenter is actually assigned to this issue
169+
const isAssigned = currentIssue.assignees && currentIssue.assignees.some(a => a.login === commenter);
170+
171+
if (!isAssigned) {
172+
console.log(`User ${commenter} is not assigned to this issue`);
173+
174+
await github.rest.issues.createComment({
175+
...repo,
176+
issue_number: issue.number,
177+
body: `@${commenter} You are not assigned to this issue.`
178+
});
179+
return;
180+
}
181+
182+
// Remove assignee
183+
console.log(`Removing ${commenter} from issue #${issue.number}`);
184+
185+
try {
186+
await github.rest.issues.removeAssignees({
187+
...repo,
188+
issue_number: issue.number,
189+
assignees: [commenter]
190+
});
191+
} catch (error) {
192+
console.log(`Failed to unassign: ${error.message}`);
193+
await github.rest.issues.createComment({
194+
...repo,
195+
issue_number: issue.number,
196+
body: `@${commenter} Failed to unassign you from this issue. Please try again or contact a maintainer.`
197+
});
198+
return;
199+
}
200+
201+
// Remove "in progress" label if no one else is assigned
202+
try {
203+
// Re-fetch issue to check remaining assignees
204+
const { data: updatedIssue } = await github.rest.issues.get({
205+
...repo,
206+
issue_number: issue.number
207+
});
208+
209+
if (!updatedIssue.assignees || updatedIssue.assignees.length === 0) {
210+
await github.rest.issues.removeLabel({
211+
...repo,
212+
issue_number: issue.number,
213+
name: 'in progress'
214+
});
215+
console.log(`Removed "in progress" label from issue #${issue.number}`);
216+
}
217+
} catch (error) {
218+
console.log(`Could not remove label: ${error.message}`);
219+
}
220+
221+
await github.rest.issues.createComment({
222+
...repo,
223+
issue_number: issue.number,
224+
body: `@${commenter} You have been unassigned from this issue. It's now available for others to claim.`
225+
});
226+
227+
console.log(`Successfully unassigned ${commenter} from issue #${issue.number}`);

0 commit comments

Comments
 (0)