Skip to content

Commit b5e1ab0

Browse files
committed
feat: add auto assign beginner auto
Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com>
1 parent f8060ea commit b5e1ab0

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Auto-assigns a Beginner issue when a user comments `/assign`.
3+
*
4+
* Architecture:
5+
* - Policy lives in lib/eligibility/*
6+
* - Messaging lives in lib/comments/*
7+
* - This file only orchestrates
8+
*/
9+
10+
const { isTeam } =
11+
require('../lib/team/has-team');
12+
13+
const { hasBeginnerEligibility } =
14+
require('../lib/eligibility/has-eligibility-02-beginner');
15+
16+
const { rejectionRouter } =
17+
require('../lib/comments/rejection-router');
18+
19+
const { assignReminder } =
20+
require('../lib/comments/reminder-to-request-assign');
21+
22+
const { alreadyAssigned } =
23+
require('../lib/comments/issue-already-assigned');
24+
25+
const BEGINNER_LABEL = 'beginner';
26+
const ASSIGN_REMINDER_MARKER = '<!-- Beginner assign reminder -->';
27+
28+
const BROWSE_URLS = {
29+
gfi: 'https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3A"Good+First+Issue"+no%3Aassignee',
30+
beginner: 'https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3Abeginner+no%3Aassignee',
31+
};
32+
33+
function requestsAssignment(body) {
34+
return typeof body === 'string' && /(^|\s)\/assign(\s|$)/i.test(body);
35+
}
36+
37+
function isBeginnerIssue(issue) {
38+
return (issue.labels ?? []).some(l => l.name === BEGINNER_LABEL);
39+
}
40+
41+
module.exports = async ({ github, context }) => {
42+
const { issue, comment } = context.payload;
43+
const { owner, repo } = context.repo;
44+
45+
if (!issue || !comment) return;
46+
if (comment.user?.type === 'Bot') return;
47+
if (!isBeginnerIssue(issue)) return;
48+
49+
const username = comment.user.login;
50+
51+
console.log('[beginner-assign] Start', {
52+
issue: issue.number,
53+
username,
54+
});
55+
56+
// ─────────────────────────────────────────────
57+
// Gentle reminder if user comments but not /assign
58+
// ─────────────────────────────────────────────
59+
if (!requestsAssignment(comment.body)) {
60+
if (
61+
!issue.assignees?.length &&
62+
!(await isTeam({ github, owner, repo, username }))
63+
) {
64+
const comments = await github.paginate(
65+
github.rest.issues.listComments,
66+
{ owner, repo, issue_number: issue.number, per_page: 100 }
67+
);
68+
69+
const alreadyReminded = comments.some(c =>
70+
c.body?.includes(ASSIGN_REMINDER_MARKER)
71+
);
72+
73+
if (!alreadyReminded) {
74+
await github.rest.issues.createComment({
75+
owner,
76+
repo,
77+
issue_number: issue.number,
78+
body:
79+
ASSIGN_REMINDER_MARKER +
80+
assignReminder(username, 'Beginner'),
81+
});
82+
83+
console.log('[beginner-assign] Posted assign reminder');
84+
}
85+
}
86+
87+
return;
88+
}
89+
90+
// ─────────────────────────────────────────────
91+
// Already assigned
92+
// ─────────────────────────────────────────────
93+
if (issue.assignees?.length) {
94+
await github.rest.issues.createComment({
95+
owner,
96+
repo,
97+
issue_number: issue.number,
98+
body: alreadyAssigned({
99+
username,
100+
assignee: `@${issue.assignees[0].login}`,
101+
browseUrl: BROWSE_URLS.beginner,
102+
tierLabel: 'Beginner',
103+
}),
104+
});
105+
return;
106+
}
107+
108+
// ─────────────────────────────────────────────
109+
// Eligibility check (POLICY)
110+
// ─────────────────────────────────────────────
111+
const result = await hasBeginnerEligibility({
112+
github,
113+
owner,
114+
repo,
115+
username,
116+
});
117+
118+
if (!result.eligible) {
119+
const body = rejectionRouter({
120+
reason: result.reason,
121+
context: result.context,
122+
username,
123+
urls: BROWSE_URLS,
124+
});
125+
126+
if (body) {
127+
await github.rest.issues.createComment({
128+
owner,
129+
repo,
130+
issue_number: issue.number,
131+
body,
132+
});
133+
}
134+
135+
return;
136+
}
137+
138+
// ─────────────────────────────────────────────
139+
// Assign issue
140+
// ─────────────────────────────────────────────
141+
await github.rest.issues.addAssignees({
142+
owner,
143+
repo,
144+
issue_number: issue.number,
145+
assignees: [username],
146+
});
147+
148+
console.log('[beginner-assign] Assigned successfully', {
149+
issue: issue.number,
150+
username,
151+
});
152+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const { beginnerRejection } = require('./difficulty-02-beginner');
2+
const { capacityLimitReached } = require('./max-assignments-reached');
3+
const { spamNonGfiAssignment } = require('./spam-restrictions');
4+
5+
const REJECTION_REASONS =
6+
require('../eligibility/rejection-reasons');
7+
8+
const rejectionRouter = ({ reason, context, username, urls }) => {
9+
switch (reason) {
10+
case REJECTION_REASONS.MISSING_GFI:
11+
return beginnerRejection({
12+
username,
13+
completedGfiCount: context.completedGfiCount,
14+
browseGfiUrl: urls.gfi,
15+
});
16+
17+
case REJECTION_REASONS.CAPACITY:
18+
return capacityLimitReached({
19+
username,
20+
openAssignedCount: context.openAssignedCount,
21+
maxAllowed: context.maxAllowed,
22+
});
23+
24+
case REJECTION_REASONS.SPAM:
25+
return spamNonGfiAssignment(username);
26+
27+
default:
28+
return null;
29+
}
30+
};
31+
32+
module.exports = { rejectionRouter };
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// lib/eligibility/rejection-reasons.js
2+
module.exports = {
3+
MISSING_GFI: 'missing_gfi',
4+
CAPACITY: 'capacity_exceeded',
5+
SPAM: 'spam_listed',
6+
};

0 commit comments

Comments
 (0)