Skip to content

Commit 9fc88dd

Browse files
authored
Merge branch 'main' into fix/bot-intermediate-assignment-guard
Signed-off-by: Parv <[email protected]>
2 parents ffe34a8 + c4d147d commit 9fc88dd

File tree

58 files changed

+1353
-1298
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1353
-1298
lines changed

.coderabbit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ reviews:
1010
high_level_summary: false # Do not summarise a pull request first as there is a walkthrough
1111
review_status: false # Do not state what kind of review as performed or why (spammy)
1212
commit_status: false # Do not state the review is in progress (spammy)
13-
collapse_walkthrough: true # Provide a walkthrough for reviewers, but collapse it (users shouldn't use this)
13+
collapse_walkthrough: false # Provide a walkthrough for reviewers
1414
related_issues: false # Do not suggest related issues (spammy)
1515
related_prs: false # Do not suggest related PRs (spammy)
1616
suggested_labels: false # Do not suggest labels for the PR (spammy)

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
blank_issues_enabled: true
22

3-
# Only show specific issue templates
4-
# issue_templates:
5-
# - name: Good First Issue
6-
# filename: 01-good_first_issue.yml
7-
#- name: Bug Report
8-
# filename: 02_bug_report.yml
9-
10-
# - name: Feature Request
11-
# filename: 03_feature_request.yml
12-
13-
# Test contact links
143
contact_links:
154
- name: Hiero Discord
165
url: https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/*
2+
==============================================================================
3+
Executes When:
4+
- Triggered by GitHub Actions workflow on event: 'issue_comment' (created).
5+
- Target: Issues specifically labeled with "beginner".
6+
7+
Goal:
8+
It acts as an automated onboarding assistant for "beginner" issues. It allows
9+
contributors to self-assign using a command and nudges new contributors who
10+
express interest but forget to assign themselves, while preventing spam.
11+
12+
------------------------------------------------------------------------------
13+
Flow: Basic Idea
14+
1. Listens for comments on issues.
15+
2. Ignores Pull Requests, Bots, and issues missing the "beginner" label.
16+
3. Detects if the user typed "/assign".
17+
- If YES: Assigns the user to the issue (if currently unassigned).
18+
- If NO: Checks if the user is an external contributor expressing interest.
19+
If so, it replies with instructions on how to use the assign command.
20+
21+
------------------------------------------------------------------------------
22+
Flow: Detailed Technical Steps
23+
24+
1️⃣ Validation & filtering
25+
- Checks payload to ensure it is an Issue Comment (not a PR).
26+
- Checks if the commenter is a BOT (e.g., github-actions). If so, exits.
27+
- Checks if the issue has the specific label "beginner". If not, exits.
28+
29+
2️⃣ Collaborator Check (isRepoCollaborator)
30+
- Action: Checks if the user is a repository collaborator.
31+
- Logic:
32+
* Collaborator (204) -> Treated as Team Member (Bot ignores them).
33+
* Non-Collaborator (404) -> Treated as External Contributor (Bot helps them).
34+
35+
3️⃣ Logic Branch A: The "/assign" Command
36+
- Trigger: User comment matches regex /(^|\s)\/assign(\s|$)/i.
37+
- Check: Is the issue already assigned?
38+
* Yes -> Alert the user that it is taken.
39+
* No -> API Call: Add commenter to 'assignees'.
40+
41+
4️⃣ Logic Branch B: The Helper Reminder
42+
- Trigger: Generic comment (e.g., "I want to work on this").
43+
- Condition 1: Issue must be unassigned.
44+
- Condition 2: Commenter must NOT be a Repo Collaborator.
45+
- Condition 3: Duplicate Check.
46+
* Scans previous comments for a hidden marker: "<!-- beginner assign reminder -->".
47+
* If found -> Exits to avoid spamming the thread.
48+
- Action: Posts a comment with the hidden marker and instructions.
49+
50+
------------------------------------------------------------------------------
51+
Parameters:
52+
- { github, context }: Standard objects provided by 'actions/github-script'.
53+
==============================================================================
54+
*/
55+
56+
const fs = require("fs");
57+
58+
const SPAM_LIST_PATH = ".github/spam-list.txt";
59+
60+
module.exports = async ({ github, context }) => {
61+
try {
62+
const { payload } = context;
63+
const issue = payload.issue;
64+
const comment = payload.comment;
65+
const repo = payload.repository;
66+
67+
// 1. Basic Validation
68+
if (!issue || !comment || !repo || issue.pull_request) {
69+
console.log("[Beginner Bot] Invalid payload or PR comment. Exiting.");
70+
return;
71+
}
72+
73+
// 1.1 Bot Check (Fix 2: Defensive Check)
74+
if (comment.user?.type === "Bot") {
75+
console.log(`[Beginner Bot] Commenter @${comment.user.login} is a bot. Exiting.`);
76+
return;
77+
}
78+
79+
// 2. Label Check (Fix 2: Defensive Check)
80+
const hasBeginnerLabel = Array.isArray(issue.labels) && issue.labels.some((label) => label.name === "beginner");
81+
if (!hasBeginnerLabel) {
82+
console.log(`[Beginner Bot] Issue #${issue.number} does not have 'beginner' label. Exiting.`);
83+
return;
84+
}
85+
86+
// 3. Collaborator Check Helper
87+
async function isRepoCollaborator(username) {
88+
try {
89+
if (username === repo.owner.login) {
90+
console.log(`[Beginner Bot] User @${username} is the repo owner.`);
91+
return true;
92+
}
93+
94+
await github.rest.repos.checkCollaborator({
95+
owner: repo.owner.login,
96+
repo: repo.name,
97+
username: username,
98+
});
99+
console.log(`[Beginner Bot] User @${username} is a confirmed repo collaborator.`);
100+
return true;
101+
} catch (error) {
102+
if (error.status === 404) {
103+
console.log(`[Beginner Bot] User @${username} is NOT a collaborator (External Contributor).`);
104+
return false;
105+
}
106+
console.log(`[Beginner Bot] Error checking collaborator status for @${username}: ${error.message}`);
107+
return false;
108+
}
109+
}
110+
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+
138+
const commenter = comment.user.login;
139+
140+
// Fix 3: Validate comment body
141+
if (!comment.body) {
142+
console.log("[Beginner Bot] Comment body is empty. Exiting.");
143+
return;
144+
}
145+
146+
const commentBody = comment.body.toLowerCase();
147+
const isAssignCommand = /(^|\s)\/assign(\s|$)/i.test(commentBody);
148+
149+
// 4. Logic Branch
150+
if (isAssignCommand) {
151+
// --- ASSIGNMENT LOGIC ---
152+
if (issue.assignees && issue.assignees.length > 0) {
153+
const currentAssignee = issue.assignees[0].login;
154+
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Ignoring /assign command.`);
155+
156+
// Fix 4: Granular Try/Catch for Comment API
157+
try {
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+
});
164+
} catch (error) {
165+
console.error(`[Beginner Bot] Failed to post already-assigned comment: ${error.message}`);
166+
}
167+
return; // Exit after warning
168+
}
169+
170+
// Block spam users from beginner issues
171+
172+
const spamUser = isSpamUser(commenter);
173+
174+
if (spamUser) {
175+
console.log(`[Beginner Bot] Spam user @${commenter} attempted to assign to beginner issue. Blocked.`);
176+
177+
try {
178+
await github.rest.issues.createComment({
179+
owner: repo.owner.login,
180+
repo: repo.name,
181+
issue_number: issue.number,
182+
body: `Hi @${commenter}, your account is currently restricted to **Good First Issues**. Please complete a Good First Issue or contact a maintainer to have restrictions reviewed.`,
183+
});
184+
} catch (error) {
185+
console.error(`[Beginner Bot] Failed to post spam restriction message: ${error.message}`);
186+
}
187+
188+
return;
189+
}
190+
191+
// Enforce Assignment Limits
192+
193+
const openCount = await getOpenAssignments(commenter);
194+
const maxAllowed = 2;
195+
196+
console.log("[Beginner Bot] Limit check:", {
197+
commenter,
198+
openCount,
199+
spamUser,
200+
maxAllowed,
201+
});
202+
203+
if (openCount >= maxAllowed) {
204+
const message = `👋 Hi @${commenter}, you already have **2 open assignments**. Please finish one before requesting another.`;
205+
206+
try {
207+
await github.rest.issues.createComment({
208+
owner: repo.owner.login,
209+
repo: repo.name,
210+
issue_number: issue.number,
211+
body: message,
212+
});
213+
} catch (error) {
214+
console.error(`[Beginner Bot] Failed to post limit warning: ${error.message}`);
215+
}
216+
217+
return;
218+
}
219+
220+
console.log(`[Beginner Bot] Assigning issue #${issue.number} to @${commenter}...`);
221+
222+
// Fix 4: Granular Try/Catch for Assign API
223+
try {
224+
await github.rest.issues.addAssignees({
225+
owner: repo.owner.login,
226+
repo: repo.name,
227+
issue_number: issue.number,
228+
assignees: [commenter],
229+
});
230+
console.log(`[Beginner Bot] Successfully assigned.`);
231+
} catch (error) {
232+
console.error(`[Beginner Bot] Failed to assign issue: ${error.message}`);
233+
}
234+
235+
} else {
236+
// --- REMINDER LOGIC ---
237+
238+
if (issue.assignees && issue.assignees.length > 0) {
239+
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Skipping reminder.`);
240+
return;
241+
}
242+
243+
if (await isRepoCollaborator(commenter)) {
244+
console.log(`[Beginner Bot] Commenter @${commenter} is a repo collaborator. Skipping reminder.`);
245+
return;
246+
}
247+
248+
// Fix 5: Updated Marker Text
249+
const REMINDER_MARKER = "<!-- beginner assign reminder -->";
250+
// FIX 6: Granular Try/Catch for List Comments API
251+
let comments;
252+
try {
253+
const { data } = await github.rest.issues.listComments({
254+
owner: repo.owner.login,
255+
repo: repo.name,
256+
issue_number: issue.number,
257+
});
258+
comments = data;
259+
} catch (error) {
260+
console.error(`[Beginner Bot] Failed to list comments: ${error.message}`);
261+
return; // Exit gracefully if we can't check for duplicates
262+
}
263+
264+
if (comments.some((c) => c.body.includes(REMINDER_MARKER))) {
265+
console.log("[Beginner Bot] Reminder already exists on this issue. Skipping.");
266+
return;
267+
}
268+
269+
console.log(`[Beginner Bot] Posting help reminder for @${commenter}...`);
270+
271+
const reminderBody = `${REMINDER_MARKER}\n👋 Hi @${commenter}! If you'd like to work on this issue, please comment \`/assign\` to get assigned.`;
272+
273+
// FIX 6: Granular Try/Catch for Create Comment API
274+
try {
275+
await github.rest.issues.createComment({
276+
owner: repo.owner.login,
277+
repo: repo.name,
278+
issue_number: issue.number,
279+
body: reminderBody,
280+
});
281+
console.log("[Beginner Bot] Reminder posted successfully.");
282+
} catch (error) {
283+
console.error(`[Beginner Bot] Failed to post reminder: ${error.message}`);
284+
}
285+
}
286+
287+
} catch (error) {
288+
// Fix 1: Top-level error handling
289+
console.error("[Beginner Bot] Unexpected error:", {
290+
message: error.message,
291+
status: error.status,
292+
issue: context.payload?.issue?.number,
293+
comment: context.payload?.comment?.id
294+
});
295+
}
296+
};

0 commit comments

Comments
 (0)