Skip to content

Commit dc0ce53

Browse files
authored
Merge pull request #111 from exploreriii/shared-libraries-guards
feat: chaining GFI auto and mentor
2 parents 41e0e48 + 50fbc45 commit dc0ce53

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

.github/scripts/bots/assign-01-gfi-auto.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const { hasGfiEligibility } = require('../lib/eligibility/has-eligibility-01-gfi
1212
const { rejectionRouter } = require('../lib/comments/rejection-router');
1313
const { assignReminder } = require('../lib/comments/reminder-to-request-assign');
1414
const { alreadyAssigned } = require('../lib/comments/issue-already-assigned');
15+
const assignMentor =
16+
require('./assign-01-gfi-mentor');
1517

1618
const GOOD_FIRST_ISSUE_LABEL = 'Good First Issue';
1719
const ASSIGN_REMINDER_MARKER = '<!-- GFI assign reminder -->';
@@ -189,6 +191,21 @@ module.exports = async ({ github, context }) => {
189191
issue_number: issue.number,
190192
assignees: [username],
191193
});
194+
console.log('[gfi-assign] Triggering mentor assignment');
195+
196+
await assignMentor({
197+
github,
198+
context: {
199+
...context,
200+
payload: {
201+
issue,
202+
assignee: {
203+
login: username,
204+
type: 'User',
205+
},
206+
},
207+
},
208+
});
192209

193210
console.log('[gfi-assign] Assigned successfully', {
194211
issue: issue.number,
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Automatically assigns a mentor when a human contributor
3+
* is assigned to a Good First Issue.
4+
*
5+
* Triggered by: issues.assigned
6+
*
7+
* Architecture:
8+
* - Comment text lives in lib/comments/assign-mentor-gfi.js
9+
* - All logic, API calls, and guards live here
10+
*/
11+
12+
const fs = require('fs');
13+
const path = require('path');
14+
15+
const {
16+
COMMENT_MARKER,
17+
buildMentorAssignmentComment,
18+
} = require('../lib/comments/assign-mentor-gfi');
19+
20+
// Defaults (can be overridden via env)
21+
const MENTOR_TEAM_ALIAS =
22+
process.env.MENTOR_TEAM_ALIAS ||
23+
'@hiero-ledger/hiero-sdk-python-triage';
24+
25+
const SUPPORT_TEAM_ALIAS =
26+
process.env.SUPPORT_TEAM_ALIAS ||
27+
'@hiero-ledger/hiero-sdk-good-first-issue-support';
28+
29+
const DEFAULT_ROSTER_FILE = '.github/mentor_roster.json';
30+
const GOOD_FIRST_ISSUE_LABEL = 'good first issue';
31+
32+
// ─────────────────────────────────────────────
33+
// Helper: label guard
34+
// ─────────────────────────────────────────────
35+
function hasGoodFirstIssueLabel(issue) {
36+
return (issue.labels ?? []).some(label => {
37+
const name =
38+
typeof label === 'string'
39+
? label
40+
: label?.name;
41+
42+
return (
43+
typeof name === 'string' &&
44+
name.toLowerCase() === GOOD_FIRST_ISSUE_LABEL
45+
);
46+
});
47+
}
48+
49+
// ─────────────────────────────────────────────
50+
// Helper: load mentor roster
51+
// ─────────────────────────────────────────────
52+
function loadMentorRoster() {
53+
const rosterPath = path.resolve(
54+
process.cwd(),
55+
process.env.MENTOR_ROSTER_PATH || DEFAULT_ROSTER_FILE
56+
);
57+
58+
console.log('[mentor-assign] Loading mentor roster:', rosterPath);
59+
60+
const raw = fs.readFileSync(rosterPath, 'utf8');
61+
const parsed = JSON.parse(raw);
62+
63+
const roster = (parsed.order ?? [])
64+
.map(entry => String(entry).trim())
65+
.filter(Boolean);
66+
67+
if (!roster.length) {
68+
throw new Error('Mentor roster is empty');
69+
}
70+
71+
return roster;
72+
}
73+
74+
// ─────────────────────────────────────────────
75+
// Helper: deterministic mentor selection
76+
// ─────────────────────────────────────────────
77+
function selectMentor(roster) {
78+
const dayIndex = Math.floor(
79+
Date.now() / (24 * 60 * 60 * 1000)
80+
);
81+
82+
return roster[dayIndex % roster.length];
83+
}
84+
85+
// ─────────────────────────────────────────────
86+
// Entry point
87+
// ─────────────────────────────────────────────
88+
module.exports = async ({ github, context }) => {
89+
try {
90+
const { issue, assignee } = context.payload;
91+
92+
// ─────────────────────────────────────────
93+
// Guard rails
94+
// ─────────────────────────────────────────
95+
if (!issue || !assignee) {
96+
console.log('[mentor-assign] Exit: missing issue or assignee');
97+
return;
98+
}
99+
100+
if (assignee.type === 'Bot') {
101+
console.log('[mentor-assign] Exit: assignee is a bot', {
102+
assignee: assignee.login,
103+
});
104+
return;
105+
}
106+
107+
if (!hasGoodFirstIssueLabel(issue)) {
108+
console.log('[mentor-assign] Exit: not a Good First Issue', {
109+
issue: issue.number,
110+
});
111+
return;
112+
}
113+
114+
const { owner, repo } = context.repo;
115+
const mentee = assignee.login;
116+
117+
console.log('[mentor-assign] Start', {
118+
issue: issue.number,
119+
mentee,
120+
});
121+
122+
// ─────────────────────────────────────────
123+
// Prevent duplicate mentor comments
124+
// ─────────────────────────────────────────
125+
const comments = await github.paginate(
126+
github.rest.issues.listComments,
127+
{
128+
owner,
129+
repo,
130+
issue_number: issue.number,
131+
per_page: 100,
132+
}
133+
);
134+
135+
const alreadyAssigned = comments.some(comment =>
136+
comment.body?.includes(COMMENT_MARKER)
137+
);
138+
139+
console.log('[mentor-assign] Existing mentor comment check', {
140+
alreadyAssigned,
141+
});
142+
143+
if (alreadyAssigned) {
144+
console.log('[mentor-assign] Exit: mentor already assigned');
145+
return;
146+
}
147+
148+
// ─────────────────────────────────────────
149+
// Select mentor
150+
// ─────────────────────────────────────────
151+
const roster = loadMentorRoster();
152+
const mentor = selectMentor(roster);
153+
154+
console.log('[mentor-assign] Mentor selected', {
155+
mentor,
156+
});
157+
158+
// ─────────────────────────────────────────
159+
// Build & post comment (from lib)
160+
// ─────────────────────────────────────────
161+
const body = buildMentorAssignmentComment({
162+
mentee,
163+
mentor,
164+
owner,
165+
repo,
166+
mentorTeamAlias: MENTOR_TEAM_ALIAS,
167+
supportTeamAlias: SUPPORT_TEAM_ALIAS,
168+
});
169+
170+
await github.rest.issues.createComment({
171+
owner,
172+
repo,
173+
issue_number: issue.number,
174+
body,
175+
});
176+
177+
console.log('[mentor-assign] Mentor comment posted successfully', {
178+
issue: issue.number,
179+
mentor,
180+
mentee,
181+
});
182+
} catch (error) {
183+
console.error('[mentor-assign] Failure', {
184+
message: error.message,
185+
});
186+
throw error;
187+
}
188+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const COMMENT_MARKER =
2+
process.env.COMMENT_MARKER || '<!-- Mentor Assignment Bot -->';
3+
4+
const MENTOR_TEAM =
5+
process.env.MENTOR_TEAM_ALIAS || '@hiero-ledger/hiero-sdk-python-triage';
6+
7+
const SUPPORT_TEAM =
8+
process.env.SUPPORT_TEAM_ALIAS ||
9+
'@hiero-ledger/hiero-sdk-good-first-issue-support';
10+
11+
const buildMentorComment = ({ mentee, mentor, owner, repo }) => `
12+
${COMMENT_MARKER}
13+
👋 Hi @${mentee}, welcome!
14+
15+
Your on-call mentor today is @${mentor} from ${MENTOR_TEAM}.
16+
17+
**How to proceed**
18+
- Review the issue carefully
19+
- Ask questions early
20+
- Share progress often
21+
22+
Need backup? ${SUPPORT_TEAM} is here too.
23+
24+
**Mentor:** @${mentor}
25+
**Mentee:** @${mentee}
26+
27+
⭐ If you enjoy this repo, consider starring it:
28+
https://github.com/${owner}/${repo}
29+
`;
30+
31+
module.exports = { buildMentorComment, COMMENT_MARKER };

0 commit comments

Comments
 (0)