Skip to content

Commit fe1cc60

Browse files
[ci:fix] Refined reusable autoassign workflow
- Reduce boilerplate by moving logic to the workflow - Quote pip install target to prevent bash glob expansion - Fix shell syntax and protect against shell injection - Return True on early skips to avoid GitHub Actions failures - Avoid running if PR is merged - Avoid suggesting to look in openwisp-utils, which is a special case. --------- Co-authored-by: Eeshu-Yadav <eeshuyadav123@gmail.com>
1 parent c3a9dd3 commit fe1cc60

9 files changed

Lines changed: 204 additions & 64 deletions

.github/actions/bot-autoassign/issue_assignment_bot.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def handle_issue_comment(self):
273273
try:
274274
if self.event_payload.get("issue", {}).get("pull_request"):
275275
print("Comment is on a PR, not an issue - skipping")
276-
return False
276+
return True
277277
comment = self.event_payload.get("comment", {})
278278
issue = self.event_payload.get("issue", {})
279279
comment_body = comment.get("body", "")
@@ -285,7 +285,7 @@ def handle_issue_comment(self):
285285
if self.is_assignment_request(comment_body):
286286
return self.respond_to_assignment_request(issue_number, commenter)
287287
print("Comment does not contain assignment request")
288-
return False
288+
return True
289289
except Exception as e:
290290
print(f"Error handling issue comment: {e}")
291291
return False
@@ -308,10 +308,13 @@ def handle_pull_request(self):
308308
# We consider the event handled even if no issues were linked
309309
return True
310310
elif action == "closed":
311-
self.unassign_issues_from_pr(pr_body, pr_author)
311+
if pr.get("merged", False):
312+
print(f"PR #{pr_number} was merged, keeping issue assignments")
313+
else:
314+
self.unassign_issues_from_pr(pr_body, pr_author)
312315
return True
313316
print(f"PR action '{action}' not handled")
314-
return False
317+
return True
315318
except Exception as e:
316319
print(f"Error handling pull request: {e}")
317320
return False
@@ -328,7 +331,7 @@ def run(self):
328331
return self.handle_pull_request()
329332
else:
330333
print(f"Event type '{self.event_name}'" " not supported")
331-
return False
334+
return True
332335
except Exception as e:
333336
print(f"Error in main execution: {e}")
334337
return False

.github/actions/bot-autoassign/pr_reopen_bot.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def handle_pr_reopen(self):
7676
print(f"Reassigned {len(reassigned)}" f" issues to {pr_author}")
7777
return True
7878
except Exception as e:
79-
print(f"Error handling PR reopen: {e}")
79+
print(f"Error handling reopened PR: {e}")
8080
return False
8181

8282
def run(self):
@@ -89,7 +89,7 @@ def run(self):
8989
return self.handle_pr_reopen()
9090
else:
9191
print(f"Event type '{self.event_name}'" " not supported")
92-
return False
92+
return True
9393
except Exception as e:
9494
print(f"Error in main execution: {e}")
9595
return False
@@ -113,15 +113,15 @@ def handle_contributor_activity(self):
113113
return False
114114
if not issue_data.get("pull_request"):
115115
print("Comment is on an issue," " not a PR, skipping")
116-
return False
116+
return True
117117
pr = self.repo.get_pull(pr_number)
118118
if not pr.user or commenter != pr.user.login:
119119
print("Comment not from PR author, skipping")
120-
return False
120+
return True
121121
labels = [label.name for label in pr.get_labels()]
122122
if "stale" not in labels:
123123
print("PR is not stale, skipping")
124-
return False
124+
return True
125125
try:
126126
pr.remove_from_labels("stale")
127127
print("Removed stale label")
@@ -169,7 +169,7 @@ def run(self):
169169
return self.handle_contributor_activity()
170170
else:
171171
print(f"Event type '{self.event_name}'" " not supported")
172-
return False
172+
return True
173173
except Exception as e:
174174
print(f"Error in main execution: {e}")
175175
return False

.github/actions/bot-autoassign/stale_pr_bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def unassign_linked_issues(self, pr):
144144
def close_stale_pr(self, pr, days_inactive):
145145
if pr.state == "closed":
146146
print(f"PR #{pr.number} is already closed, skipping")
147-
return False
147+
return True
148148
try:
149149
pr_author = pr.user.login if pr.user else None
150150
if not pr_author:

.github/actions/bot-autoassign/tests/test_issue_assignment_bot.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_skip_pr_comment(self, bot_env):
291291
},
292292
}
293293
)
294-
assert not bot.handle_issue_comment()
294+
assert bot.handle_issue_comment()
295295

296296
def test_non_assignment_comment(self, bot_env):
297297
bot = IssueAssignmentBot()
@@ -304,7 +304,7 @@ def test_non_assignment_comment(self, bot_env):
304304
},
305305
}
306306
)
307-
assert not bot.handle_issue_comment()
307+
assert bot.handle_issue_comment()
308308

309309
def test_no_payload(self, bot_env):
310310
bot = IssueAssignmentBot()
@@ -373,6 +373,22 @@ def test_closed(self, bot_env):
373373
assert bot.handle_pull_request()
374374
mock_issue.remove_from_assignees.assert_called_once_with("testuser")
375375

376+
def test_merged_does_not_unassign(self, bot_env):
377+
bot = IssueAssignmentBot()
378+
bot.load_event_payload(
379+
{
380+
"action": "closed",
381+
"pull_request": {
382+
"number": 100,
383+
"user": {"login": "testuser"},
384+
"body": "Fixes #123",
385+
"merged": True,
386+
},
387+
}
388+
)
389+
assert bot.handle_pull_request()
390+
bot_env["repo"].get_issue.assert_not_called()
391+
376392
def test_unsupported_action(self, bot_env):
377393
bot = IssueAssignmentBot()
378394
bot.load_event_payload(
@@ -385,7 +401,7 @@ def test_unsupported_action(self, bot_env):
385401
},
386402
}
387403
)
388-
assert not bot.handle_pull_request()
404+
assert bot.handle_pull_request()
389405

390406

391407
class TestRun:
@@ -431,7 +447,7 @@ def test_pull_request_event(self, bot_env):
431447
def test_unsupported_event(self, bot_env):
432448
bot = IssueAssignmentBot()
433449
bot.event_name = "push"
434-
assert not bot.run()
450+
assert bot.run()
435451

436452
def test_no_github_client(self, bot_env):
437453
bot = IssueAssignmentBot()

.github/actions/bot-autoassign/tests/test_pr_reopen_bot.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_handle_pr_reopen_no_payload(self, bot_env):
108108
def test_run_unsupported_event(self, bot_env):
109109
bot = PRReopenBot()
110110
bot.event_name = "push"
111-
assert not bot.run()
111+
assert bot.run()
112112

113113

114114
class TestPRActivityBot:
@@ -160,7 +160,7 @@ def test_handle_contributor_activity_not_author(self, bot_env):
160160
mock_pr = Mock()
161161
mock_pr.user.login = "testuser"
162162
bot_env["repo"].get_pull.return_value = mock_pr
163-
assert not bot.handle_contributor_activity()
163+
assert bot.handle_contributor_activity()
164164

165165
def test_handle_contributor_activity_pr_not_stale(self, bot_env):
166166
bot = PRActivityBot()
@@ -177,7 +177,7 @@ def test_handle_contributor_activity_pr_not_stale(self, bot_env):
177177
mock_pr.user.login = "testuser"
178178
mock_pr.get_labels.return_value = []
179179
bot_env["repo"].get_pull.return_value = mock_pr
180-
assert not bot.handle_contributor_activity()
180+
assert bot.handle_contributor_activity()
181181

182182
def test_handle_contributor_activity_not_pr(self, bot_env):
183183
bot = PRActivityBot()
@@ -190,7 +190,7 @@ def test_handle_contributor_activity_not_pr(self, bot_env):
190190
"comment": {"user": {"login": "testuser"}},
191191
}
192192
)
193-
assert not bot.handle_contributor_activity()
193+
assert bot.handle_contributor_activity()
194194

195195
def test_handle_contributor_activity_no_payload(self, bot_env):
196196
bot = PRActivityBot()
@@ -199,4 +199,4 @@ def test_handle_contributor_activity_no_payload(self, bot_env):
199199
def test_run_unsupported_event(self, bot_env):
200200
bot = PRActivityBot()
201201
bot.event_name = "push"
202-
assert not bot.run()
202+
assert bot.run()

.github/actions/bot-autoassign/tests/test_stale_pr_bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def test_already_closed(self, bot_env):
231231
bot = StalePRBot()
232232
mock_pr = Mock()
233233
mock_pr.state = "closed"
234-
assert not bot.close_stale_pr(mock_pr, 60)
234+
assert bot.close_stale_pr(mock_pr, 60)
235235
mock_pr.create_issue_comment.assert_not_called()
236236
mock_pr.edit.assert_not_called()
237237

.github/workflows/bot-autoassign-pr-issue-link.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ concurrency:
1515

1616
jobs:
1717
auto-assign-issue:
18+
if: github.event.action != 'closed' || github.event.pull_request.merged == false
1819
runs-on: ubuntu-latest
1920
steps:
2021
- name: Generate GitHub App token
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Reusable Bot Autoassign
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
bot_command:
7+
required: true
8+
type: string
9+
description: "The bot to execute (e.g., 'issue_assignment', 'pr_reopen', 'stale_pr')"
10+
secrets:
11+
OPENWISP_BOT_APP_ID:
12+
required: true
13+
OPENWISP_BOT_PRIVATE_KEY:
14+
required: true
15+
16+
jobs:
17+
run-bot:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Generate GitHub App token
21+
id: generate-token
22+
uses: actions/create-github-app-token@v2
23+
with:
24+
app-id: ${{ secrets.OPENWISP_BOT_APP_ID }}
25+
private-key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
26+
27+
- name: Checkout openwisp-utils
28+
uses: actions/checkout@v6
29+
with:
30+
repository: openwisp/openwisp-utils
31+
ref: master
32+
path: openwisp-utils
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v6
36+
with:
37+
python-version: "3.13"
38+
39+
- name: Install dependencies
40+
run: pip install -e 'openwisp-utils/.[github_actions]'
41+
42+
- name: Execute bot script
43+
env:
44+
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
45+
REPOSITORY: ${{ github.repository }}
46+
GITHUB_EVENT_NAME: ${{ github.event_name }}
47+
BOT_COMMAND: ${{ inputs.bot_command }}
48+
run: |
49+
if [ -n "$GITHUB_EVENT_PATH" ]; then
50+
python openwisp-utils/.github/actions/bot-autoassign/__main__.py "$BOT_COMMAND" "$GITHUB_EVENT_PATH"
51+
else
52+
python openwisp-utils/.github/actions/bot-autoassign/__main__.py "$BOT_COMMAND"
53+
fi

0 commit comments

Comments
 (0)