Skip to content

Commit 39f20a7

Browse files
authored
Merge pull request #53 from exploreriii/test-mentor-assingment-3
ci: test mentor assingment 3
2 parents 19a3a61 + d9e283e commit 39f20a7

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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-python-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+
try {
62+
const response = await github.rest.search.issuesAndPullRequests({
63+
q: query,
64+
per_page: 1,
65+
});
66+
return (response.data.total_count || 0) === 0;
67+
} catch (error) {
68+
console.log(`Unable to determine merged PRs for ${login}:`, error.message || error);
69+
// Return null (skip assignment) on API errors to avoid workflow failure while preserving accurate logging
70+
return null;
71+
}
72+
}
73+
74+
function buildComment({ mentee, mentor, owner, repo }) {
75+
const repoUrl = `https://github.com/${owner}/${repo}`;
76+
77+
return `${COMMENT_MARKER}
78+
👋 Hi @${mentee}, welcome to the Hiero Python SDK community!
79+
80+
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.
81+
82+
**How to get started**
83+
- Review the issue description and any linked docs
84+
- Share updates early and ask @${mentor} anything right here
85+
- Keep the feedback loop short so we can support you quickly
86+
87+
Need more backup? ${SUPPORT_TEAM_ALIAS} is also on standby to cheer you on.
88+
89+
**Mentor:** @${mentor}
90+
**Mentee:** @${mentee}
91+
92+
If you're enjoying the SDK, consider ⭐️ [starring the repository](${repoUrl}) so it's easy to find later.
93+
94+
Happy building!
95+
— Python SDK Team`;
96+
}
97+
98+
module.exports = async ({ github, context }) => {
99+
try {
100+
const issue = context.payload.issue;
101+
const assignee = context.payload.assignee;
102+
103+
if (!issue?.number || !assignee?.login) {
104+
return console.log('No issue or assignee found in payload. Skipping.');
105+
}
106+
107+
if (assignee.type === 'Bot') {
108+
return console.log(`Assignee ${assignee.login} is a bot. Skipping.`);
109+
}
110+
111+
if (!hasGoodFirstIssueLabel(issue)) {
112+
return console.log(`Issue #${issue.number} is not labeled as Good First Issue. Skipping.`);
113+
}
114+
115+
const { owner, repo } = context.repo;
116+
const mentee = assignee.login;
117+
118+
// Ensure we haven't already posted a mentor assignment comment
119+
const existingComments = await github.paginate(github.rest.issues.listComments, {
120+
owner,
121+
repo,
122+
issue_number: issue.number,
123+
per_page: 100,
124+
});
125+
126+
if (existingComments.some((comment) => comment.body?.includes(COMMENT_MARKER))) {
127+
return console.log(`Mentor assignment comment already exists on issue #${issue.number}. Skipping.`);
128+
}
129+
130+
const isNewStarter = await isNewContributor(github, owner, repo, mentee);
131+
132+
if (isNewStarter === null) {
133+
return console.log(`Unable to confirm whether ${mentee} is a new contributor due to API error. Skipping mentor assignment.`);
134+
}
135+
136+
if (!isNewStarter) {
137+
return console.log(`${mentee} already has merged contributions. Skipping mentor assignment.`);
138+
}
139+
140+
const roster = loadMentorRoster();
141+
const mentor = selectMentor(roster);
142+
143+
console.log(`Assigning mentor @${mentor} to mentee @${mentee} for issue #${issue.number}.`);
144+
145+
const comment = buildComment({ mentee, mentor, owner, repo });
146+
147+
try {
148+
await github.rest.issues.createComment({
149+
owner,
150+
repo,
151+
issue_number: issue.number,
152+
body: comment,
153+
});
154+
} catch (error) {
155+
const message = error instanceof Error ? error.message : String(error);
156+
157+
const freshComments = await github.paginate(github.rest.issues.listComments, {
158+
owner,
159+
repo,
160+
issue_number: issue.number,
161+
per_page: 100,
162+
});
163+
164+
if (freshComments.some((existing) => existing.body?.includes(COMMENT_MARKER))) {
165+
return console.log(`Mentor assignment comment already exists on issue #${issue.number} after concurrent run. Skipping. (${message})`);
166+
}
167+
168+
throw error;
169+
}
170+
} catch (error) {
171+
const message = error instanceof Error ? error.message : String(error);
172+
console.log(`❌ Mentor assignment failed: ${message}`);
173+
throw error;
174+
}
175+
};
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
@@ -14,6 +14,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1414
- Added a GitHub Actions workflow to validate broken Markdown links in pull requests.
1515
- Added method chaining examples to the developer training guide (`docs/sdk_developers/training/coding_token_transactions.md`) (#1194)
1616
- Added documentation explaining how to pin GitHub Actions to specific commit SHAs (`docs/sdk_developers/how-to-pin-github-actions.md`)(#1211)
17+
- Added mentor assignment workflow and script to pair new Good First Issue assignees with on-call mentors.
1718
- examples/mypy.ini for stricter type checking in example scripts
1819
- Added a GitHub Actions workflow that reminds contributors to link pull requests to issues.
1920
- Added `__str__` and `__repr__` methods to `AccountInfo` class for improved logging and debugging experience (#1098)

0 commit comments

Comments
 (0)