Skip to content

Commit 9a5e10e

Browse files
committed
fix: bot return type eligibility with reasons
Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com>
1 parent b5e1ab0 commit 9a5e10e

6 files changed

Lines changed: 276 additions & 332 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Auto-assigns a Good First 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 { hasGfiEligibility } =
14+
require('../lib/eligibility/has-eligibility-01-gfi');
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 GOOD_FIRST_ISSUE_LABEL = 'Good First Issue';
26+
const ASSIGN_REMINDER_MARKER = '<!-- GFI 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+
};
31+
32+
function requestsAssignment(body) {
33+
return typeof body === 'string' && /(^|\s)\/assign(\s|$)/i.test(body);
34+
}
35+
36+
function isGfiIssue(issue) {
37+
return (issue.labels ?? []).some(l => l.name === GOOD_FIRST_ISSUE_LABEL);
38+
}
39+
40+
module.exports = async ({ github, context }) => {
41+
const { issue, comment } = context.payload;
42+
const { owner, repo } = context.repo;
43+
44+
if (!issue || !comment) return;
45+
if (comment.user?.type === 'Bot') return;
46+
if (!isGfiIssue(issue)) return;
47+
48+
const username = comment.user.login;
49+
50+
console.log('[gfi-assign] Start', {
51+
issue: issue.number,
52+
username,
53+
});
54+
55+
// ─────────────────────────────────────────────
56+
// Gentle reminder if user comments but not /assign
57+
// (exclude team members)
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+
{
67+
owner,
68+
repo,
69+
issue_number: issue.number,
70+
per_page: 100,
71+
}
72+
);
73+
74+
const alreadyReminded = comments.some(c =>
75+
c.body?.includes(ASSIGN_REMINDER_MARKER)
76+
);
77+
78+
if (!alreadyReminded) {
79+
await github.rest.issues.createComment({
80+
owner,
81+
repo,
82+
issue_number: issue.number,
83+
body:
84+
ASSIGN_REMINDER_MARKER +
85+
assignReminder(username, 'Good First'),
86+
});
87+
88+
console.log('[gfi-assign] Posted assign reminder');
89+
}
90+
}
91+
92+
return;
93+
}
94+
95+
// ─────────────────────────────────────────────
96+
// Already assigned
97+
// ─────────────────────────────────────────────
98+
if (issue.assignees?.length) {
99+
await github.rest.issues.createComment({
100+
owner,
101+
repo,
102+
issue_number: issue.number,
103+
body: alreadyAssigned({
104+
username,
105+
assignee: `@${issue.assignees[0].login}`,
106+
browseUrl: BROWSE_URLS.gfi,
107+
tierLabel: 'Good First',
108+
}),
109+
});
110+
return;
111+
}
112+
113+
// ─────────────────────────────────────────────
114+
// Eligibility check (POLICY)
115+
// ─────────────────────────────────────────────
116+
const result = await hasGfiEligibility({
117+
github,
118+
owner,
119+
repo,
120+
username,
121+
});
122+
123+
if (!result.eligible) {
124+
const body = rejectionRouter({
125+
reason: result.reason,
126+
context: result.context,
127+
username,
128+
urls: BROWSE_URLS,
129+
});
130+
131+
if (body) {
132+
await github.rest.issues.createComment({
133+
owner,
134+
repo,
135+
issue_number: issue.number,
136+
body,
137+
});
138+
}
139+
140+
return;
141+
}
142+
143+
// ─────────────────────────────────────────────
144+
// Assign issue
145+
// ─────────────────────────────────────────────
146+
await github.rest.issues.addAssignees({
147+
owner,
148+
repo,
149+
issue_number: issue.number,
150+
assignees: [username],
151+
});
152+
153+
console.log('[gfi-assign] Assigned successfully', {
154+
issue: issue.number,
155+
username,
156+
});
157+
};
Lines changed: 10 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,7 @@
1-
/**
2-
* Determines whether a contributor is eligible to be assigned
3-
* a Good First Issue (GFI) in this repository.
4-
*
5-
* ELIGIBILITY RULES:
6-
* - Repository team members (triage / write / maintain / admin)
7-
* are always eligible and bypass all checks.
8-
* - Normal contributors may have up to `MAX_OPEN_ISSUES_NORMAL`
9-
* open issue assignments.
10-
* - Contributors listed in the spam list may have up to
11-
* `MAX_OPEN_ISSUES_SPAM_LIST` open issue assignments.
12-
* - Assignment is allowed only if the contributor currently has
13-
* fewer than their maximum allowed open assignments.
14-
*
15-
* IMPORTANT NOTES:
16-
* - This helper is policy-only.
17-
* - It does NOT assign issues.
18-
* - It does NOT post comments.
19-
* - Callers must invoke this before attempting assignment.
20-
*
21-
* @param {Object} params
22-
* @param {import('@actions/github').GitHub} params.github - Authenticated GitHub client
23-
* @param {string} params.owner - Repository owner
24-
* @param {string} params.repo - Repository name
25-
* @param {string} params.username - GitHub username to check
26-
* @returns {Promise<boolean>} Whether the contributor may be assigned a GFI
27-
*/
281
const { isTeam } = require('../team/has-team');
292
const { isOnSpamList } = require('../counts/is-on-spam-list');
303
const { countOpenAssignedIssues } = require('../counts/count-open-assigned-issues');
4+
const REJECTION_REASONS = require('./rejection-reasons');
315

326
const MAX_OPEN_ISSUES_NORMAL = 2;
337
const MAX_OPEN_ISSUES_SPAM_LIST = 1;
@@ -38,70 +12,32 @@ const hasGfiEligibility = async ({
3812
repo,
3913
username,
4014
}) => {
41-
// Log the start of the eligibility check for traceability
42-
console.log('[has-gfi-eligibility] Start check:', {
43-
owner,
44-
repo,
45-
username,
46-
});
47-
48-
// Repository team members bypass all GFI eligibility checks.
4915
if (await isTeam({ github, owner, repo, username })) {
50-
console.log('[has-gfi-eligibility] Skipped: user is team member', {
51-
username,
52-
});
53-
return true;
16+
return { eligible: true };
5417
}
5518

56-
// Determine whether the contributor is listed in the spam list.
57-
// Spam-listed contributors are subject to stricter limits.
58-
const isSpamListed = await isOnSpamList({
59-
github,
60-
owner,
61-
repo,
62-
username,
63-
});
19+
const isSpamListed = await isOnSpamList({ github, owner, repo, username });
6420

65-
// Select the appropriate maximum based on spam status.
6621
const maxAllowed = isSpamListed
6722
? MAX_OPEN_ISSUES_SPAM_LIST
6823
: MAX_OPEN_ISSUES_NORMAL;
6924

70-
// Count how many open issues are currently assigned to the contributor.
71-
// This helper fails open and returns a large number on error,
72-
// which causes this eligibility check to conservatively fail.
7325
const openAssignedCount = await countOpenAssignedIssues({
7426
github,
7527
owner,
7628
repo,
7729
username,
7830
});
7931

80-
console.log('[has-gfi-eligibility] Capacity evaluation:', {
81-
username,
82-
isSpamListed,
83-
openAssignedCount,
84-
maxAllowed,
85-
});
86-
87-
// Enforce assignment capacity limits
8832
if (openAssignedCount >= maxAllowed) {
89-
console.log('[has-gfi-eligibility] Exit: assignment limit reached', {
90-
username,
91-
openAssignedCount,
92-
maxAllowed,
93-
});
94-
return false;
33+
return {
34+
eligible: false,
35+
reason: REJECTION_REASONS.CAPACITY,
36+
context: { openAssignedCount, maxAllowed },
37+
};
9538
}
9639

97-
// Contributor is under their assignment limit and may be assigned a GFI.
98-
console.log('[has-gfi-eligibility] Success: contributor eligible for GFI', {
99-
username,
100-
});
101-
102-
return true;
40+
return { eligible: true };
10341
};
10442

105-
module.exports = {
106-
hasGfiEligibility,
107-
};
43+
module.exports = { hasGfiEligibility };

0 commit comments

Comments
 (0)