forked from flashinfer-ai/flashinfer
-
Notifications
You must be signed in to change notification settings - Fork 0
330 lines (279 loc) · 12.9 KB
/
issue-claim.yml
File metadata and controls
330 lines (279 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# Issue self-claim workflow for external contributors
# Commands:
# !claim - Self-assign an unassigned issue (anyone)
# !assign @user - Assign a specific user (maintainers only)
name: Issue Claim
on:
issue_comment:
types: [created]
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
# Note: The collaborator invitation (PUT /repos/.../collaborators/...)
# requires admin-level access. This workflow uses FLASHINFER_BOT_TOKEN (a PAT
# with admin scope) for all API calls; the permissions below only apply to the
# default GITHUB_TOKEN which is not used.
permissions:
contents: read
issues: write
jobs:
handle-claim:
# Only run on issue comments (not PRs) that start with !claim or !assign
# Skip comments from bots
if: |
github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
github.event.comment.user.type != 'Bot' &&
(startsWith(github.event.comment.body, '!claim') || startsWith(github.event.comment.body, '!assign'))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Parse command
id: parse
env:
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
FIRST_LINE=$(echo "$COMMENT_BODY" | head -1)
if echo "$FIRST_LINE" | grep -q '^!claim\b'; then
echo "command=claim" >> "$GITHUB_OUTPUT"
echo "target_user=${COMMENTER}" >> "$GITHUB_OUTPUT"
elif echo "$FIRST_LINE" | grep -q '^!assign[[:space:]]'; then
# Extract username: strip !assign prefix, optional @, take first word
TARGET=$(echo "$FIRST_LINE" | sed 's/^!assign[[:space:]]*//')
TARGET="${TARGET#@}"
TARGET="${TARGET%% *}"
# Validate GitHub username format: alphanumeric and hyphens, 1-39 chars
if [ -n "$TARGET" ] && echo "$TARGET" | grep -q '^[a-zA-Z0-9][a-zA-Z0-9-]*$' && [ ${#TARGET} -le 39 ]; then
echo "command=assign" >> "$GITHUB_OUTPUT"
echo "target_user=$TARGET" >> "$GITHUB_OUTPUT"
else
echo "command=assign-no-user" >> "$GITHUB_OUTPUT"
echo "target_user=" >> "$GITHUB_OUTPUT"
fi
elif echo "$FIRST_LINE" | grep -q '^!assign$'; then
echo "command=assign-no-user" >> "$GITHUB_OUTPUT"
echo "target_user=" >> "$GITHUB_OUTPUT"
else
echo "command=unknown" >> "$GITHUB_OUTPUT"
echo "target_user=" >> "$GITHUB_OUTPUT"
fi
- name: Handle !claim
if: steps.parse.outputs.command == 'claim'
env:
GH_TOKEN: ${{ secrets.FLASHINFER_BOT_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
TARGET_USER: ${{ steps.parse.outputs.target_user }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo "::error::FLASHINFER_BOT_TOKEN secret is not set"
exit 1
fi
# Check if issue already has assignees
ASSIGNEE_COUNT=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${REPO}/issues/${ISSUE_NUMBER}" \
--jq '.assignees | length')
if [ "$ASSIGNEE_COUNT" -gt 0 ]; then
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='confused'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="This issue is already assigned. If you'd like to take over, ask a maintainer to use \`!assign @${TARGET_USER}\`."
exit 0
fi
# Try to assign the user and verify it succeeded
ASSIGN_RESPONSE=$(gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/assignees" \
-f "assignees[]=${TARGET_USER}" 2>&1) && ASSIGN_OK=true || ASSIGN_OK=false
if [ "$ASSIGN_OK" = "true" ]; then
IS_ASSIGNED=$(echo "$ASSIGN_RESPONSE" | jq --arg u "$TARGET_USER" \
'[.assignees[].login] | map(ascii_downcase) | contains([$u | ascii_downcase])')
else
IS_ASSIGNED="false"
fi
if [ "$IS_ASSIGNED" = "true" ]; then
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='+1'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="Issue assigned to @${TARGET_USER}."
else
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='confused'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="Could not assign @${TARGET_USER}. A maintainer can use \`!assign @${TARGET_USER}\` to send an invitation."
fi
- name: Handle !assign
if: steps.parse.outputs.command == 'assign'
env:
GH_TOKEN: ${{ secrets.FLASHINFER_BOT_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
COMMENTER: ${{ github.event.comment.user.login }}
TARGET_USER: ${{ steps.parse.outputs.target_user }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo "::error::FLASHINFER_BOT_TOKEN secret is not set"
exit 1
fi
# Check commenter's repository permission (admin/maintain required)
PERMISSION=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${REPO}/collaborators/${COMMENTER}/permission" \
--jq '.permission' 2>&1) || PERMISSION="none"
if [ "$PERMISSION" != "admin" ] && [ "$PERMISSION" != "maintain" ]; then
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='confused'
exit 0
fi
# Remove existing assignees first
CURRENT_ASSIGNEES=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${REPO}/issues/${ISSUE_NUMBER}" \
--jq '[.assignees[].login]')
if [ "$CURRENT_ASSIGNEES" != "[]" ] && [ "$CURRENT_ASSIGNEES" != "null" ]; then
gh api -X DELETE \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/assignees" \
--input <(echo "$CURRENT_ASSIGNEES" | jq '{assignees: .}') 2>&1 || true
fi
# Try to assign target user
assign_or_invite() {
local user="$1"
local invite_msg="$2"
ASSIGN_RESPONSE=$(gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/assignees" \
-f "assignees[]=${user}" 2>&1) && ASSIGN_OK=true || ASSIGN_OK=false
if [ "$ASSIGN_OK" = "true" ]; then
IS_ASSIGNED=$(echo "$ASSIGN_RESPONSE" | jq --arg u "$user" \
'[.assignees[].login] | map(ascii_downcase) | contains([$u | ascii_downcase])')
else
IS_ASSIGNED="false"
fi
if [ "$IS_ASSIGNED" = "true" ]; then
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='+1'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="Issue assigned to @${user}."
else
INVITE_RESPONSE=$(gh api -X PUT \
"/repos/${REPO}/collaborators/${user}" \
-f permission='triage' 2>&1) && INVITE_OK=true || INVITE_OK=false
if [ "$INVITE_OK" = "true" ]; then
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='+1'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="$invite_msg"
else
echo "::warning::Failed to invite ${user}: ${INVITE_RESPONSE}"
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='confused'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="Failed to send a repository invitation to @${user}. A maintainer can retry with \`!assign @${user}\`."
fi
fi
}
assign_or_invite "$TARGET_USER" \
"@${TARGET_USER} has been sent a repository invitation. They'll be automatically assigned once they [accept the invitation](https://github.com/notifications)."
- name: Handle !assign with no username
if: steps.parse.outputs.command == 'assign-no-user'
env:
GH_TOKEN: ${{ secrets.FLASHINFER_BOT_TOKEN }}
COMMENT_ID: ${{ github.event.comment.id }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo "::error::FLASHINFER_BOT_TOKEN secret is not set"
exit 1
fi
gh api -X POST \
"/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content='confused'
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="Usage: \`!assign @username\`"
auto-assign-on-accept:
# Poll for users who accepted repo invitations after !assign
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check pending invitations
env:
GH_TOKEN: ${{ secrets.FLASHINFER_BOT_TOKEN }}
REPO: ${{ github.repository }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo "::error::FLASHINFER_BOT_TOKEN secret is not set"
exit 1
fi
# Search for open issues with !assign comments updated in the last 30 days
SINCE=$(date -u -d '30 days ago' '+%Y-%m-%d')
SEARCH_RESULTS=$(gh api \
-H "Accept: application/vnd.github+json" \
"/search/issues?q=repo:${REPO}+is:issue+is:open+no:assignee+%22!assign+%40%22+in:comments+updated:>=${SINCE}&per_page=50" \
--jq '.items[].number') || true
if [ -z "$SEARCH_RESULTS" ]; then
echo "No open issues with !assign comments found."
exit 0
fi
for ISSUE_NUMBER in $SEARCH_RESULTS; do
echo "--- Checking issue #${ISSUE_NUMBER} ---"
# Get the most recent !assign @user comment (last one wins)
LAST_ASSIGN=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments?per_page=100" \
--jq '[.[] | select(.body | test("^!assign\\s+@?"))] | last | .body')
if [ -z "$LAST_ASSIGN" ] || [ "$LAST_ASSIGN" = "null" ]; then
echo "No !assign comments found in issue #${ISSUE_NUMBER}, skipping."
continue
fi
# Extract username from the most recent !assign comment
USERNAME=$(echo "$LAST_ASSIGN" | \
sed -n 's/^!assign[[:space:]]*@\?\([a-zA-Z0-9][a-zA-Z0-9-]*\).*/\1/p')
if [ -z "$USERNAME" ]; then
echo "Could not extract username from !assign comment, skipping."
continue
fi
echo "Most recent !assign target: $USERNAME"
# Check if user is now a collaborator (accepted invitation)
if gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${REPO}/collaborators/${USERNAME}" \
--silent 2>/dev/null; then
echo "User $USERNAME is now a collaborator. Assigning to issue #${ISSUE_NUMBER}."
ASSIGN_RESPONSE=$(gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/assignees" \
-f "assignees[]=${USERNAME}" 2>&1) && ASSIGN_OK=true || ASSIGN_OK=false
if [ "$ASSIGN_OK" = "true" ]; then
IS_ASSIGNED=$(echo "$ASSIGN_RESPONSE" | jq --arg u "$USERNAME" \
'[.assignees[].login] | map(ascii_downcase) | contains([$u | ascii_downcase])')
else
IS_ASSIGNED="false"
fi
if [ "$IS_ASSIGNED" = "true" ]; then
gh api -X POST \
"/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-f body="@${USERNAME} has accepted the repository invitation and has been automatically assigned to this issue."
else
echo "::warning::Failed to assign ${USERNAME} to issue #${ISSUE_NUMBER}: ${ASSIGN_RESPONSE}"
fi
else
echo "User $USERNAME is not yet a collaborator, skipping."
fi
done