Skip to content

Commit 3bfea64

Browse files
authored
Merge pull request #59 from exploreriii/mentor-test-2
test: Mentor test 2
2 parents db04ee4 + b89dc1a commit 3bfea64

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

.github/mentor_roster.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"order": [
3+
"emiliyank",
4+
"MonaaEid",
5+
"aceppaluni",
6+
"AntonioCeppellini",
7+
"exploreriii",
8+
"Adityarya11",
9+
"tech0priyanshu",
10+
"Akshat8510"
11+
]
12+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const COMMENT_MARKER = process.env.COMMENT_MARKER || '<!-- Mentor Assignment Bot -->';
5+
const MENTOR_TEAM_ALIAS = process.env.MENTOR_TEAM_ALIAS || '@hiero-ledger/hiero-sdk-python-triage';
6+
const SUPPORT_TEAM_ALIAS = process.env.SUPPORT_TEAM_ALIAS || '@hiero-ledger/hiero-sdk-good-first-issue-support';
7+
const DEFAULT_ROSTER_FILE = '.github/mentor_roster.json';
8+
9+
function loadMentorRoster() {
10+
const rosterPath = path.resolve(
11+
process.cwd(),
12+
process.env.MENTOR_ROSTER_PATH || DEFAULT_ROSTER_FILE,
13+
);
14+
15+
let fileContents;
16+
try {
17+
fileContents = fs.readFileSync(rosterPath, 'utf8');
18+
} catch (error) {
19+
throw new Error(`Failed to read mentor roster at ${rosterPath}: ${error.message}`);
20+
}
21+
22+
try {
23+
const parsed = JSON.parse(fileContents);
24+
const rawOrder = Array.isArray(parsed?.order) ? parsed.order : [];
25+
const roster = rawOrder
26+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
27+
.filter(Boolean);
28+
29+
if (!roster.length) {
30+
throw new Error('Mentor roster is empty after filtering.');
31+
}
32+
33+
return roster;
34+
} catch (error) {
35+
throw new Error(`Failed to parse mentor roster JSON: ${error.message}`);
36+
}
37+
}
38+
39+
function selectMentor(roster) {
40+
if (!Array.isArray(roster) || roster.length === 0) {
41+
throw new Error('Mentor roster must contain at least one entry.');
42+
}
43+
44+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
45+
const dayNumber = Math.floor(Date.now() / MILLISECONDS_PER_DAY); // UTC day index
46+
const index = dayNumber % roster.length;
47+
48+
return roster[index];
49+
}
50+
51+
function hasGoodFirstIssueLabel(issue) {
52+
return (issue.labels || []).some((label) => {
53+
const name = typeof label === 'string' ? label : label?.name;
54+
return typeof name === 'string' && name.toLowerCase() === 'good first issue';
55+
});
56+
}
57+
58+
async function isNewContributor(github, owner, repo, login) {
59+
const query = `repo:${owner}/${repo} type:pr state:closed is:merged author:${login}`;
60+
61+
const hasToken = Boolean(process.env.GITHUB_TOKEN || process.env.GH_TOKEN);
62+
console.log(`Mentor assignment search query: ${query}`);
63+
console.log(`GitHub token present: ${hasToken}`);
64+
65+
try {
66+
const response = await github.rest.search.issuesAndPullRequests({
67+
q: query,
68+
per_page: 1,
69+
});
70+
const totalCount = response?.data?.total_count || 0;
71+
console.log(`Merged PR count for ${login}: ${totalCount}`);
72+
const isNewStarter = totalCount === 0;
73+
console.log(`Is ${login} considered a new starter? ${isNewStarter}`);
74+
return isNewStarter;
75+
} catch (error) {
76+
console.log(`Unable to determine merged PRs for ${login}:`, error.message || error);
77+
// Return null (skip assignment) on API errors to avoid workflow failure while preserving accurate logging
78+
return null;
79+
}
80+
}
81+
82+
function buildComment({ mentee, mentor, owner, repo }) {
83+
const repoUrl = `https://github.com/${owner}/${repo}`;
84+
85+
return `${COMMENT_MARKER}
86+
👋 Hi @${mentee}, welcome to the Hiero Python SDK community!
87+
88+
You've been assigned this Good First Issue, and today’s on-call mentor from ${MENTOR_TEAM_ALIAS} is @${mentor}. They're here to help you land a great first contribution.
89+
90+
**How to get started**
91+
- Review the issue description and any linked docs
92+
- Share updates early and ask @${mentor} anything right here
93+
- Keep the feedback loop short so we can support you quickly
94+
95+
Need more backup? ${SUPPORT_TEAM_ALIAS} is also on standby to cheer you on.
96+
97+
**Mentor:** @${mentor}
98+
**Mentee:** @${mentee}
99+
100+
If you're enjoying the SDK, consider ⭐️ [starring the repository](${repoUrl}) so it's easy to find later.
101+
102+
Happy building!
103+
— Python SDK Team`;
104+
}
105+
106+
module.exports = async ({ github, context }) => {
107+
try {
108+
const issue = context.payload.issue;
109+
const assignee = context.payload.assignee;
110+
111+
if (!issue?.number || !assignee?.login) {
112+
return console.log('No issue or assignee found in payload. Skipping.');
113+
}
114+
115+
if (assignee.type === 'Bot') {
116+
return console.log(`Assignee ${assignee.login} is a bot. Skipping.`);
117+
}
118+
119+
if (!hasGoodFirstIssueLabel(issue)) {
120+
return console.log(`Issue #${issue.number} is not labeled as Good First Issue. Skipping.`);
121+
}
122+
123+
const { owner, repo } = context.repo;
124+
const mentee = assignee.login;
125+
126+
// Ensure we haven't already posted a mentor assignment comment
127+
const existingComments = await github.paginate(github.rest.issues.listComments, {
128+
owner,
129+
repo,
130+
issue_number: issue.number,
131+
per_page: 100,
132+
});
133+
134+
if (existingComments.some((comment) => comment.body?.includes(COMMENT_MARKER))) {
135+
return console.log(`Mentor assignment comment already exists on issue #${issue.number}. Skipping.`);
136+
}
137+
138+
const isNewStarter = await isNewContributor(github, owner, repo, mentee);
139+
140+
if (isNewStarter === null) {
141+
return console.log(`Unable to confirm whether ${mentee} is a new contributor due to API error. Skipping mentor assignment.`);
142+
}
143+
144+
if (!isNewStarter) {
145+
return console.log(`${mentee} already has merged contributions. Skipping mentor assignment.`);
146+
}
147+
148+
const roster = loadMentorRoster();
149+
const mentor = selectMentor(roster);
150+
151+
console.log(`Assigning mentor @${mentor} to mentee @${mentee} for issue #${issue.number}.`);
152+
153+
const comment = buildComment({ mentee, mentor, owner, repo });
154+
155+
try {
156+
await github.rest.issues.createComment({
157+
owner,
158+
repo,
159+
issue_number: issue.number,
160+
body: comment,
161+
});
162+
} catch (error) {
163+
const message = error instanceof Error ? error.message : String(error);
164+
165+
const freshComments = await github.paginate(github.rest.issues.listComments, {
166+
owner,
167+
repo,
168+
issue_number: issue.number,
169+
per_page: 100,
170+
});
171+
172+
if (freshComments.some((existing) => existing.body?.includes(COMMENT_MARKER))) {
173+
return console.log(`Mentor assignment comment already exists on issue #${issue.number} after concurrent run. Skipping. (${message})`);
174+
}
175+
176+
throw error;
177+
}
178+
} catch (error) {
179+
const message = error instanceof Error ? error.message : String(error);
180+
console.log(`❌ Mentor assignment failed: ${message}`);
181+
throw error;
182+
}
183+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Automatically pairs first-time contributors on Good First Issues with a human mentor.
2+
name: Mentor Assignment Bot
3+
4+
on:
5+
issues:
6+
types:
7+
- assigned
8+
9+
permissions:
10+
issues: write
11+
contents: read
12+
13+
jobs:
14+
assign-mentor:
15+
if: contains(github.event.issue.labels.*.name, 'Good First Issue')
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Harden the runner
19+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
20+
with:
21+
egress-policy: audit
22+
23+
- name: Checkout repository
24+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
25+
26+
- name: Assign mentor to new starter
27+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
with:
31+
script: |
32+
const script = require('./.github/scripts/bot-mentor-assignment.js');
33+
await script({ github, context });

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1616
- Added a GitHub Actions workflow to validate broken Markdown links in pull requests.
1717
- Added method chaining examples to the developer training guide (`docs/sdk_developers/training/coding_token_transactions.md`) (#1194)
1818
- Added documentation explaining how to pin GitHub Actions to specific commit SHAs (`docs/sdk_developers/how-to-pin-github-actions.md`)(#1211)
19+
- Added mentor assignment workflow and script to pair new Good First Issue assignees with on-call mentors.
1920
- examples/mypy.ini for stricter type checking in example scripts
2021
- Added a GitHub Actions workflow that reminds contributors to link pull requests to issues.
2122
- Added `__str__` and `__repr__` methods to `AccountInfo` class for improved logging and debugging experience (#1098)

0 commit comments

Comments
 (0)