Skip to content

Commit 9482acd

Browse files
authored
feat: add auto-assignment bot for beginner issues (hiero-ledger#1417)
Signed-off-by: Parth J Chaudhary <parthchaudhary.jc@yahoo.com>
1 parent 7fb2654 commit 9482acd

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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+
module.exports = async ({ github, context }) => {
56+
try {
57+
const { payload } = context;
58+
const issue = payload.issue;
59+
const comment = payload.comment;
60+
const repo = payload.repository;
61+
62+
// 1. Basic Validation
63+
if (!issue || !comment || !repo || issue.pull_request) {
64+
console.log("[Beginner Bot] Invalid payload or PR comment. Exiting.");
65+
return;
66+
}
67+
68+
// 1.1 Bot Check (Fix 2: Defensive Check)
69+
if (comment.user?.type === "Bot") {
70+
console.log(`[Beginner Bot] Commenter @${comment.user.login} is a bot. Exiting.`);
71+
return;
72+
}
73+
74+
// 2. Label Check (Fix 2: Defensive Check)
75+
const hasBeginnerLabel = Array.isArray(issue.labels) && issue.labels.some((label) => label.name === "beginner");
76+
if (!hasBeginnerLabel) {
77+
console.log(`[Beginner Bot] Issue #${issue.number} does not have 'beginner' label. Exiting.`);
78+
return;
79+
}
80+
81+
// 3. Collaborator Check Helper
82+
async function isRepoCollaborator(username) {
83+
try {
84+
if (username === repo.owner.login) {
85+
console.log(`[Beginner Bot] User @${username} is the repo owner.`);
86+
return true;
87+
}
88+
89+
await github.rest.repos.checkCollaborator({
90+
owner: repo.owner.login,
91+
repo: repo.name,
92+
username: username,
93+
});
94+
95+
console.log(`[Beginner Bot] User @${username} is a confirmed repo collaborator.`);
96+
return true;
97+
} catch (error) {
98+
if (error.status === 404) {
99+
console.log(`[Beginner Bot] User @${username} is NOT a collaborator (External Contributor).`);
100+
return false;
101+
}
102+
console.log(`[Beginner Bot] Error checking collaborator status for @${username}: ${error.message}`);
103+
return false;
104+
}
105+
}
106+
107+
const commenter = comment.user.login;
108+
109+
// Fix 3: Validate comment body
110+
if (!comment.body) {
111+
console.log("[Beginner Bot] Comment body is empty. Exiting.");
112+
return;
113+
}
114+
115+
const commentBody = comment.body.toLowerCase();
116+
const isAssignCommand = /(^|\s)\/assign(\s|$)/i.test(commentBody);
117+
118+
// 4. Logic Branch
119+
if (isAssignCommand) {
120+
// --- ASSIGNMENT LOGIC ---
121+
if (issue.assignees && issue.assignees.length > 0) {
122+
const currentAssignee = issue.assignees[0].login;
123+
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Ignoring /assign command.`);
124+
125+
// Fix 4: Granular Try/Catch for Comment API
126+
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+
});
133+
} catch (error) {
134+
console.error(`[Beginner Bot] Failed to post already-assigned comment: ${error.message}`);
135+
}
136+
return; // Exit after warning
137+
}
138+
139+
console.log(`[Beginner Bot] Assigning issue #${issue.number} to @${commenter}...`);
140+
141+
// Fix 4: Granular Try/Catch for Assign API
142+
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.`);
150+
} catch (error) {
151+
console.error(`[Beginner Bot] Failed to assign issue: ${error.message}`);
152+
}
153+
154+
} else {
155+
// --- REMINDER LOGIC ---
156+
157+
if (issue.assignees && issue.assignees.length > 0) {
158+
console.log(`[Beginner Bot] Issue #${issue.number} is already assigned. Skipping reminder.`);
159+
return;
160+
}
161+
162+
if (await isRepoCollaborator(commenter)) {
163+
console.log(`[Beginner Bot] Commenter @${commenter} is a repo collaborator. Skipping reminder.`);
164+
return;
165+
}
166+
167+
// Fix 5: Updated Marker Text
168+
const REMINDER_MARKER = "<!-- beginner assign reminder -->";
169+
// FIX 6: Granular Try/Catch for List Comments API
170+
let comments;
171+
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;
178+
} catch (error) {
179+
console.error(`[Beginner Bot] Failed to list comments: ${error.message}`);
180+
return; // Exit gracefully if we can't check for duplicates
181+
}
182+
183+
if (comments.some((c) => c.body.includes(REMINDER_MARKER))) {
184+
console.log("[Beginner Bot] Reminder already exists on this issue. Skipping.");
185+
return;
186+
}
187+
188+
console.log(`[Beginner Bot] Posting help reminder for @${commenter}...`);
189+
190+
const reminderBody = `${REMINDER_MARKER}\n👋 Hi @${commenter}! If you'd like to work on this issue, please comment \`/assign\` to get assigned.`;
191+
192+
// FIX 6: Granular Try/Catch for Create Comment API
193+
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.");
201+
} catch (error) {
202+
console.error(`[Beginner Bot] Failed to post reminder: ${error.message}`);
203+
}
204+
}
205+
206+
} catch (error) {
207+
// Fix 1: Top-level error handling
208+
console.error("[Beginner Bot] Unexpected error:", {
209+
message: error.message,
210+
status: error.status,
211+
issue: context.payload?.issue?.number,
212+
comment: context.payload?.comment?.id
213+
});
214+
}
215+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Beginner Assign on /assign
2+
3+
on:
4+
issue_comment:
5+
types:
6+
- created
7+
8+
permissions:
9+
issues: write
10+
contents: read
11+
12+
jobs:
13+
beginner-assign:
14+
# Only run on issue comments (not PR comments)
15+
if: github.event.issue.pull_request == null
16+
17+
runs-on: ubuntu-latest
18+
19+
# Prevent race conditions unique to beginner assignment
20+
concurrency:
21+
group: beginner-assign-${{ github.event.issue.number }}
22+
cancel-in-progress: false
23+
24+
steps:
25+
- name: Harden runner
26+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
27+
with:
28+
egress-policy: audit
29+
30+
- name: Checkout repository
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32+
33+
- name: Run Beginner /assign handler
34+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0
35+
with:
36+
script: |
37+
const script = require('./.github/scripts/bot-beginner-assign-on-comment.js');
38+
await script({ github, context });

CHANGELOG.md

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

99
### Added
10+
- Auto-assignment bot for beginner-labeled issues with `/assign` command support and helpful reminders. (#1368)
1011
- Added comprehensive docstring to `FeeAssessmentMethod` enum explaining inclusive vs exclusive fee assessment methods with usage examples. (#1391)
1112
- Added comprehensive docstring to `TokenType` enum explaining fungible vs non-fungible tokens with practical use cases. (#1392)
1213

0 commit comments

Comments
 (0)