Skip to content

Commit bafc6ce

Browse files
authored
Added auto-assign github workflow (#88)
1 parent 844fd8b commit bafc6ce

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

.github/workflows/auto-assign.yml

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
name: Auto Assign & Unassign (Org-wide Limit)
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
jobs:
8+
handle-comment:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
issues: write
12+
pull-requests: write
13+
14+
steps:
15+
- name: Handle /assign or /unassign
16+
uses: actions/github-script@v7
17+
with:
18+
github-token: ${{ secrets.ORG_ACCESS_TOKEN }}
19+
script: |
20+
const comment = context.payload.comment;
21+
const body = comment.body.trim();
22+
const user = comment.user.login;
23+
const association = context.payload.comment.author_association;
24+
const issue = context.payload.issue || context.payload.pull_request;
25+
const issueNumber = issue.number;
26+
const { owner, repo } = context.repo;
27+
28+
// Ignore bots only
29+
if (comment.user.type === "Bot") {
30+
console.log("Ignoring bot");
31+
return;
32+
}
33+
34+
const isAssign = body.startsWith("/assign");
35+
const isUnassign = body.startsWith("/unassign");
36+
37+
if (!isAssign && !isUnassign) {
38+
console.log("Not an /assign or /unassign command");
39+
return;
40+
}
41+
42+
if (isUnassign) {
43+
const currentAssignees = issue.assignees.map(a => a.login);
44+
if (!currentAssignees.includes(user)) {
45+
console.log(`${user} is not assigned, skipping unassign`);
46+
return;
47+
}
48+
49+
try {
50+
await github.rest.issues.removeAssignees({
51+
owner,
52+
repo,
53+
issue_number: issueNumber,
54+
assignees: [user],
55+
});
56+
57+
await github.rest.issues.createComment({
58+
owner,
59+
repo,
60+
issue_number: issueNumber,
61+
body: `✅ Unassigned @${user} successfully.`,
62+
});
63+
64+
console.log(`Unassigned ${user} from #${issueNumber}`);
65+
} catch (error) {
66+
console.error(`Error unassigning user ${user} from issue #${issueNumber}:`, error);
67+
68+
// Attempt to create a fallback comment about the failure
69+
try {
70+
await github.rest.issues.createComment({
71+
owner,
72+
repo,
73+
issue_number: issueNumber,
74+
body: `❌ Failed to unassign @${user}. Please try again or contact a maintainer.`,
75+
});
76+
} catch (commentError) {
77+
console.error(`Failed to create error comment for user ${user} on issue #${issueNumber}:`, commentError);
78+
}
79+
}
80+
return;
81+
}
82+
83+
// For /assign
84+
if (issue.assignees.length > 0) {
85+
console.log("Already assigned, skipping.");
86+
return;
87+
}
88+
89+
console.log(`Checking org-wide assignments for ${user}...`);
90+
let totalAssigned = 0;
91+
92+
try {
93+
// Fetch all repos for the org
94+
const repos = await github.paginate(github.rest.repos.listForOrg, {
95+
org: owner,
96+
type: "all",
97+
per_page: 100,
98+
});
99+
100+
for (const r of repos) {
101+
const assignedIssues = await github.paginate(
102+
github.rest.issues.listForRepo,
103+
{
104+
owner,
105+
repo: r.name,
106+
state: "open",
107+
assignee: user,
108+
per_page: 100,
109+
}
110+
);
111+
totalAssigned += assignedIssues.length;
112+
}
113+
} catch (error) {
114+
console.error("Error fetching org-wide assignments:", error);
115+
116+
// Notify the user that the org-wide check failed
117+
try {
118+
await github.rest.issues.createComment({
119+
owner,
120+
repo,
121+
issue_number: issueNumber,
122+
body: `❌ **Org-wide assignment check failed**\n\n` +
123+
`Could not verify how many issues @${user} currently has assigned across the organization.\n\n` +
124+
`**Error:** ${error.message || 'Unknown error'}\n\n` +
125+
`Please contact a maintainer or try again later.`,
126+
});
127+
} catch (commentError) {
128+
console.error(`Failed to post error comment for user ${user} on issue #${issueNumber}:`, commentError);
129+
}
130+
131+
// Mark the workflow run as failed
132+
core.setFailed(`Org-wide assignment check failed: ${error.message || error}`);
133+
return;
134+
}
135+
136+
console.log(`${user} currently has ${totalAssigned} open issues assigned across ${owner}`);
137+
138+
// KNOWN RACE CONDITION: There is a time window between calculating totalAssigned
139+
// above and actually adding the assignee below where another workflow run (triggered
140+
// by a different /assign comment) can assign the same user to a different issue.
141+
// This distributed execution limitation means the limit can be transiently exceeded.
142+
//
143+
// Mitigation options:
144+
// 1. Accept small transient violations - simplest approach, limit is eventually
145+
// enforced and violations are rare/temporary in practice.
146+
// 2. Use a centralized locking service (e.g., Redis, DynamoDB) to serialize
147+
// assignment decisions across workflow runs.
148+
// 3. Implement retry/backoff: re-check totalAssigned immediately before addAssignees,
149+
// and if another assignment snuck in, abort and notify the user.
150+
// 4. Use GitHub's assignment as a "lock" - assign first, then check and unassign
151+
// if over limit (but this creates noise in notifications).
152+
//
153+
// Current implementation: Option 1 (accept transient violations for simplicity).
154+
if (totalAssigned >= 2) {
155+
const message = `⚠️ @${user} already has ${totalAssigned} open assignments across the org. Please finish or unassign before taking new ones.`;
156+
await github.rest.issues.createComment({
157+
owner,
158+
repo,
159+
issue_number: issueNumber,
160+
body: message,
161+
});
162+
console.log("Limit reached, skipping assignment");
163+
return;
164+
}
165+
166+
try {
167+
await github.rest.issues.addAssignees({
168+
owner,
169+
repo,
170+
issue_number: issueNumber,
171+
assignees: [user],
172+
});
173+
174+
await github.rest.issues.createComment({
175+
owner,
176+
repo,
177+
issue_number: issueNumber,
178+
body: `✅ Assigned @${user} successfully.`,
179+
});
180+
181+
console.log(`Assigned ${user} to #${issueNumber}`);
182+
} catch (error) {
183+
console.error(`Error assigning user ${user} to issue #${issueNumber}:`, error);
184+
185+
// Attempt to create a failure comment to inform the user
186+
try {
187+
await github.rest.issues.createComment({
188+
owner,
189+
repo,
190+
issue_number: issueNumber,
191+
body: `❌ **Assignment failed**\n\n` +
192+
`Could not assign @${user} to this issue.\n\n` +
193+
`**Error:** ${error.message || 'Unknown error'}\n\n` +
194+
`Please try again or contact a maintainer.`,
195+
});
196+
} catch (commentError) {
197+
console.error(`Failed to post error comment for user ${user} on issue #${issueNumber}:`, commentError);
198+
// Fall back to marking the workflow as failed if we can't even comment
199+
core.setFailed(`Assignment failed for ${user} on issue #${issueNumber}: ${error.message || error}`);
200+
}
201+
}

0 commit comments

Comments
 (0)