Skip to content

Commit 42ddae5

Browse files
committed
feat: add guards directly to auto assign beginner and gfi
Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com>
1 parent 264d954 commit 42ddae5

3 files changed

Lines changed: 185 additions & 51 deletions

File tree

.github/scripts/bot-beginner-assign-on-comment.js

Lines changed: 109 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ Parameters:
5252
- { github, context }: Standard objects provided by 'actions/github-script'.
5353
==============================================================================
5454
*/
55+
56+
const fs = require("fs");
57+
58+
const SPAM_LIST_PATH = ".github/spam-list.txt";
59+
5560
module.exports = async ({ github, context }) => {
5661
try {
5762
const { payload } = context;
@@ -82,16 +87,15 @@ module.exports = async ({ github, context }) => {
8287
async function isRepoCollaborator(username) {
8388
try {
8489
if (username === repo.owner.login) {
85-
console.log(`[Beginner Bot] User @${username} is the repo owner.`);
86-
return true;
90+
console.log(`[Beginner Bot] User @${username} is the repo owner.`);
91+
return true;
8792
}
8893

8994
await github.rest.repos.checkCollaborator({
9095
owner: repo.owner.login,
9196
repo: repo.name,
9297
username: username,
9398
});
94-
9599
console.log(`[Beginner Bot] User @${username} is a confirmed repo collaborator.`);
96100
return true;
97101
} catch (error) {
@@ -104,12 +108,39 @@ module.exports = async ({ github, context }) => {
104108
}
105109
}
106110

111+
// NEW: Spam + Assignment Limit Helpers (Layered In)
112+
113+
function isSpamUser(username) {
114+
if (!fs.existsSync(SPAM_LIST_PATH)) return false;
115+
116+
const list = fs.readFileSync(SPAM_LIST_PATH, "utf8")
117+
.split("\n")
118+
.map(l => l.trim())
119+
.filter(l => l && !l.startsWith("#"));
120+
121+
return list.includes(username);
122+
}
123+
124+
async function getOpenAssignments(username) {
125+
const issues = await github.paginate(
126+
github.rest.issues.listForRepo,
127+
{
128+
owner: repo.owner.login,
129+
repo: repo.name,
130+
assignee: username,
131+
state: "open",
132+
per_page: 100,
133+
}
134+
);
135+
return issues.length;
136+
}
137+
107138
const commenter = comment.user.login;
108139

109140
// Fix 3: Validate comment body
110141
if (!comment.body) {
111-
console.log("[Beginner Bot] Comment body is empty. Exiting.");
112-
return;
142+
console.log("[Beginner Bot] Comment body is empty. Exiting.");
143+
return;
113144
}
114145

115146
const commentBody = comment.body.toLowerCase();
@@ -119,44 +150,76 @@ module.exports = async ({ github, context }) => {
119150
if (isAssignCommand) {
120151
// --- ASSIGNMENT LOGIC ---
121152
if (issue.assignees && issue.assignees.length > 0) {
122-
const currentAssignee = issue.assignees[0].login;
153+
const currentAssignee = issue.assignees[0].login;
123154
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Ignoring /assign command.`);
124-
155+
125156
// Fix 4: Granular Try/Catch for Comment API
126157
try {
127-
await github.rest.issues.createComment({
128-
owner: repo.owner.login,
129-
repo: repo.name,
130-
issue_number: issue.number,
131-
body: `👋 Hi @${commenter}, thanks for your interest! This issue is already assigned to @${currentAssignee}, but we'd love your help on another one. You can find more "beginner" issues [here](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3Abeginner%20no%3Aassignee).`,
132-
});
158+
await github.rest.issues.createComment({
159+
owner: repo.owner.login,
160+
repo: repo.name,
161+
issue_number: issue.number,
162+
body: `👋 Hi @${commenter}, thanks for your interest! This issue is already assigned to @${currentAssignee}, but we'd love your help on another one. You can find more "beginner" issues [here](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3Abeginner%20no%3Aassignee).`,
163+
});
133164
} catch (error) {
134-
console.error(`[Beginner Bot] Failed to post already-assigned comment: ${error.message}`);
165+
console.error(`[Beginner Bot] Failed to post already-assigned comment: ${error.message}`);
135166
}
136167
return; // Exit after warning
137168
}
138169

170+
// Enforce Assignment Limits
171+
172+
const openCount = await getOpenAssignments(commenter);
173+
const spamUser = isSpamUser(commenter);
174+
const maxAllowed = spamUser ? 1 : 2;
175+
176+
console.log("[Beginner Bot] Limit check:", {
177+
commenter,
178+
openCount,
179+
spamUser,
180+
maxAllowed,
181+
});
182+
183+
if (openCount >= maxAllowed) {
184+
const message = spamUser
185+
? `👋 Hi @${commenter}, you are limited to **1 active beginner issue** at a time. Please complete your current assignment before requesting another.`
186+
: `👋 Hi @${commenter}, you already have **2 open assignments**. Please finish one before requesting another.`;
187+
188+
try {
189+
await github.rest.issues.createComment({
190+
owner: repo.owner.login,
191+
repo: repo.name,
192+
issue_number: issue.number,
193+
body: message,
194+
});
195+
} catch (error) {
196+
console.error(`[Beginner Bot] Failed to post limit warning: ${error.message}`);
197+
}
198+
199+
return;
200+
}
201+
139202
console.log(`[Beginner Bot] Assigning issue #${issue.number} to @${commenter}...`);
140-
203+
141204
// Fix 4: Granular Try/Catch for Assign API
142205
try {
143-
await github.rest.issues.addAssignees({
144-
owner: repo.owner.login,
145-
repo: repo.name,
146-
issue_number: issue.number,
147-
assignees: [commenter],
148-
});
149-
console.log(`[Beginner Bot] Successfully assigned.`);
206+
await github.rest.issues.addAssignees({
207+
owner: repo.owner.login,
208+
repo: repo.name,
209+
issue_number: issue.number,
210+
assignees: [commenter],
211+
});
212+
console.log(`[Beginner Bot] Successfully assigned.`);
150213
} catch (error) {
151-
console.error(`[Beginner Bot] Failed to assign issue: ${error.message}`);
214+
console.error(`[Beginner Bot] Failed to assign issue: ${error.message}`);
152215
}
153216

154217
} else {
155218
// --- REMINDER LOGIC ---
156-
219+
157220
if (issue.assignees && issue.assignees.length > 0) {
158-
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Skipping reminder.`);
159-
return;
221+
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Skipping reminder.`);
222+
return;
160223
}
161224

162225
if (await isRepoCollaborator(commenter)) {
@@ -165,41 +228,41 @@ module.exports = async ({ github, context }) => {
165228
}
166229

167230
// Fix 5: Updated Marker Text
168-
const REMINDER_MARKER = "<!-- beginner assign reminder -->";
231+
const REMINDER_MARKER = "<!-- beginner assign reminder -->";
169232
// FIX 6: Granular Try/Catch for List Comments API
170233
let comments;
171234
try {
172-
const { data } = await github.rest.issues.listComments({
173-
owner: repo.owner.login,
174-
repo: repo.name,
175-
issue_number: issue.number,
176-
});
177-
comments = data;
235+
const { data } = await github.rest.issues.listComments({
236+
owner: repo.owner.login,
237+
repo: repo.name,
238+
issue_number: issue.number,
239+
});
240+
comments = data;
178241
} catch (error) {
179-
console.error(`[Beginner Bot] Failed to list comments: ${error.message}`);
180-
return; // Exit gracefully if we can't check for duplicates
242+
console.error(`[Beginner Bot] Failed to list comments: ${error.message}`);
243+
return; // Exit gracefully if we can't check for duplicates
181244
}
182-
245+
183246
if (comments.some((c) => c.body.includes(REMINDER_MARKER))) {
184247
console.log("[Beginner Bot] Reminder already exists on this issue. Skipping.");
185248
return;
186249
}
187250

188251
console.log(`[Beginner Bot] Posting help reminder for @${commenter}...`);
189-
252+
190253
const reminderBody = `${REMINDER_MARKER}\n👋 Hi @${commenter}! If you'd like to work on this issue, please comment \`/assign\` to get assigned.`;
191-
254+
192255
// FIX 6: Granular Try/Catch for Create Comment API
193256
try {
194-
await github.rest.issues.createComment({
195-
owner: repo.owner.login,
196-
repo: repo.name,
197-
issue_number: issue.number,
198-
body: reminderBody,
199-
});
200-
console.log("[Beginner Bot] Reminder posted successfully.");
257+
await github.rest.issues.createComment({
258+
owner: repo.owner.login,
259+
repo: repo.name,
260+
issue_number: issue.number,
261+
body: reminderBody,
262+
});
263+
console.log("[Beginner Bot] Reminder posted successfully.");
201264
} catch (error) {
202-
console.error(`[Beginner Bot] Failed to post reminder: ${error.message}`);
265+
console.error(`[Beginner Bot] Failed to post reminder: ${error.message}`);
203266
}
204267
}
205268

.github/scripts/bot-gfi-assign-on-comment.js

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,43 @@
44
// Posts a comment if the issue is already assigned.
55
// All other validation and additional GFI comments are handled by other existing bots which can be refactored with time.
66

7+
const fs = require('fs'); // For spam list
8+
79
const GOOD_FIRST_ISSUE_LABEL = 'Good First Issue';
810
const UNASSIGNED_GFI_SEARCH_URL =
911
'https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Good%20First%20Issue%22%20no%3Aassignee';
1012

13+
const SPAM_LIST_PATH = '.github/spam-list.txt';
14+
15+
/// --------------------
16+
/// NEW HELPERS (LIMIT ENFORCEMENT)
17+
/// --------------------
18+
19+
function isSpamUser(username) {
20+
if (!fs.existsSync(SPAM_LIST_PATH)) return false;
21+
22+
const list = fs.readFileSync(SPAM_LIST_PATH, 'utf8')
23+
.split('\n')
24+
.map(l => l.trim())
25+
.filter(l => l && !l.startsWith('#'));
26+
27+
return list.includes(username);
28+
}
29+
30+
async function getOpenAssignments({ github, owner, repo, username }) {
31+
const issues = await github.paginate(
32+
github.rest.issues.listForRepo,
33+
{
34+
owner,
35+
repo,
36+
assignee: username,
37+
state: 'open',
38+
per_page: 100,
39+
}
40+
);
41+
return issues.length;
42+
}
43+
1144
/// HELPERS FOR ASSIGNING ///
1245

1346
/**
@@ -31,7 +64,7 @@ function commentRequestsAssignment(body) {
3164
*/
3265
function issueIsGoodFirstIssue(issue) {
3366
const labels = issue?.labels?.map(label => label.name) ?? [];
34-
const isGfi = labels.some(label =>
67+
const isGfi = labels.some(label =>
3568
typeof label === 'string' && label.toLowerCase() === GOOD_FIRST_ISSUE_LABEL.toLowerCase()
3669
);
3770

@@ -131,7 +164,7 @@ async function isRepoCollaborator({ github, owner, repo, username }) {
131164
}
132165
throw error; // unexpected error
133166
}
134-
167+
135168
}
136169

137170

@@ -255,6 +288,43 @@ module.exports = async ({ github, context }) => {
255288
return;
256289
}
257290

291+
// -------------------------------
292+
// ENFORCE ASSIGNMENT LIMITS
293+
// -------------------------------
294+
295+
const openCount = await getOpenAssignments({
296+
github,
297+
owner,
298+
repo,
299+
username: requesterUsername,
300+
});
301+
302+
const spamUser = isSpamUser(requesterUsername);
303+
const maxAllowed = spamUser ? 1 : 2;
304+
305+
console.log('[gfi-assign] Limit check:', {
306+
requesterUsername,
307+
openCount,
308+
spamUser,
309+
maxAllowed,
310+
});
311+
312+
if (openCount >= maxAllowed) {
313+
const message = spamUser
314+
? `Hi @${requesterUsername}, you are limited to **1 active Good First Issue** at a time. Please complete your current assignment before requesting another.`
315+
: `Hi @${requesterUsername}, you already have **2 open assignments**. Please finish one before requesting another.`;
316+
317+
await github.rest.issues.createComment({
318+
owner,
319+
repo,
320+
issue_number: issueNumber,
321+
body: message,
322+
});
323+
324+
console.log('[gfi-assign] Assignment blocked due to limit');
325+
return;
326+
}
327+
258328
console.log('[gfi-assign] Assigning issue to requester');
259329

260330
// All validations passed and user has requested assignment on a GFI
@@ -272,9 +342,9 @@ module.exports = async ({ github, context }) => {
272342
// Chain mentor assignment after successful GFI assignment
273343
try {
274344
const assignMentor = require('./bot-mentor-assignment.js');
275-
await assignMentor({
276-
github,
277-
context,
345+
await assignMentor({
346+
github,
347+
context,
278348
assignee: { login: requesterUsername, type: 'User' } // Pass freshly-assigned username
279349
});
280350
console.log('[gfi-assign] Mentor assignment chained successfully');

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
102102
### Changed
103103

104104
- Enable CodeRabbit walkthrough mode by default to improve PR review visibility (#1439)
105+
- Move assignment guards to be directly inside the gfi and beginner auto assign
105106
- Remove the commented out blocks in config.yml (#1435)
106107
- Renamed `.github/scripts/check_advanced_requirement.sh` to `bot-advanced-check.sh` for workflow consistency (#1341)
107108
- Added global review instructions to CodeRabbit configuration to limit reviews to issue/PR scope and prevent scope creep [#1373]

0 commit comments

Comments
 (0)