Skip to content

Commit c105dfe

Browse files
authored
Merge branch 'main' into fix-gfi-notification
Signed-off-by: Abhijeet <abhijeetsaharan2236@gmail.com>
2 parents 9f35bc0 + 97de974 commit c105dfe

10 files changed

Lines changed: 801 additions & 83 deletions
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
module.exports = async ({ github, context, core }) => {
2+
const { payload } = context;
3+
4+
// Get PR information from automatic pull_request_target trigger
5+
let prNumber = payload.pull_request?.number;
6+
let prBody = payload.pull_request?.body || '';
7+
8+
// Manual workflow_dispatch is no longer supported - inputs were removed
9+
// Only automatic triggers from merged PRs will work
10+
const repoOwner = context.repo.owner;
11+
const repoName = context.repo.repo;
12+
13+
if (!prNumber) {
14+
core.info('No PR number found, skipping');
15+
return;
16+
}
17+
18+
core.info(`Processing PR #${prNumber}`);
19+
20+
// Parse PR body to find linked issues
21+
const MAX_PR_BODY_LENGTH = 50000; // Reasonable limit for PR body
22+
if (prBody.length > MAX_PR_BODY_LENGTH) {
23+
core.warning(`PR body exceeds ${MAX_PR_BODY_LENGTH} characters, truncating for parsing`);
24+
prBody = prBody.substring(0, MAX_PR_BODY_LENGTH);
25+
}
26+
const issueRegex = /(fixes|closes|resolves|fix|close|resolve)\s+(?:[\w-]+\/[\w-]+)?#(\d+)/gi;
27+
const matches = [...prBody.matchAll(issueRegex)];
28+
29+
if (matches.length === 0) {
30+
core.info('No linked issues found in PR body');
31+
return;
32+
}
33+
34+
// Get the first linked issue number
35+
const issueNumber = parseInt(matches[0][2]);
36+
core.info(`Found linked issue #${issueNumber}`);
37+
38+
try {
39+
// Fetch issue details
40+
const { data: issue } = await github.rest.issues.get({
41+
owner: repoOwner,
42+
repo: repoName,
43+
issue_number: issueNumber,
44+
});
45+
46+
// Normalize and check issue labels (case-insensitive)
47+
const labelNames = issue.labels.map(label => label.name.toLowerCase());
48+
const labelSet = new Set(labelNames);
49+
core.info(`Issue labels: ${labelNames.join(', ')}`);
50+
51+
// Determine issue difficulty level
52+
const difficultyLevels = {
53+
beginner: labelSet.has('beginner'),
54+
goodFirstIssue: labelSet.has('good first issue'),
55+
intermediate: labelSet.has('intermediate'),
56+
advanced: labelSet.has('advanced'),
57+
};
58+
59+
// Skip if intermediate or advanced
60+
if (difficultyLevels.intermediate || difficultyLevels.advanced) {
61+
core.info('Issue is intermediate or advanced level, skipping recommendation');
62+
return;
63+
}
64+
65+
// Only proceed for Good First Issue or beginner issues
66+
if (!difficultyLevels.goodFirstIssue && !difficultyLevels.beginner) {
67+
core.info('Issue is not a Good First Issue or beginner issue, skipping');
68+
return;
69+
}
70+
71+
let recommendedIssues = [];
72+
73+
if (difficultyLevels.goodFirstIssue) {
74+
// Recommend beginner issues first, then Good First Issues
75+
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'beginner');
76+
if (recommendedIssues.length === 0) {
77+
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'good first issue');
78+
}
79+
} else if (difficultyLevels.beginner) {
80+
// Recommend beginner or Good First Issues
81+
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'beginner');
82+
if (recommendedIssues.length === 0) {
83+
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'good first issue');
84+
}
85+
}
86+
87+
// Generate and post comment
88+
await generateAndPostComment(github, context, core, prNumber, recommendedIssues, difficultyLevels.goodFirstIssue);
89+
90+
} catch (error) {
91+
core.setFailed(`Error processing issue #${issueNumber}: ${error.message}`);
92+
}
93+
};
94+
95+
async function searchIssues(github, core, owner, repo, label) {
96+
try {
97+
const query = `repo:${owner}/${repo} is:issue is:open label:"${label}" no:assignee`;
98+
core.info(`Searching for issues with query: ${query}`);
99+
100+
const { data: searchResult } = await github.rest.search.issuesAndPullRequests({
101+
q: query,
102+
per_page: 5,
103+
});
104+
105+
core.info(`Found ${searchResult.items.length} issues with label "${label}"`);
106+
return searchResult.items;
107+
} catch (error) {
108+
core.warning(`Error searching for issues with label "${label}": ${error.message}`);
109+
return [];
110+
}
111+
}
112+
113+
async function generateAndPostComment(github, context, core, prNumber, recommendedIssues, wasGoodFirstIssue) {
114+
const marker = '<!-- next-issue-bot-marker -->';
115+
116+
// Build comment content
117+
let comment = `${marker}\n\n🎉 **Congratulations on completing a beginner/Good First Issue!**\n\n`;
118+
comment += `Thank you for your contribution to the Hiero Python SDK! We're excited to have you as part of our community.\n\n`;
119+
120+
if (recommendedIssues.length > 0) {
121+
comment += `Here are some ${wasGoodFirstIssue ? 'beginner-level' : 'similar'} issues you might be interested in working on next:\n\n`;
122+
123+
// Sanitize title: escape markdown link syntax and special characters
124+
const sanitizeTitle = (title) => title
125+
.replace(/\[/g, '\\[')
126+
.replace(/\]/g, '\\]')
127+
.replace(/\(/g, '\\(')
128+
.replace(/\)/g, '\\)');
129+
130+
recommendedIssues.slice(0, 5).forEach((issue, index) => {
131+
comment += `${index + 1}. [${sanitizeTitle(issue.title)}](${issue.html_url})\n`;
132+
if (issue.body && issue.body.length > 0) {
133+
// Sanitize: strip HTML, normalize whitespace, escape markdown links
134+
const sanitized = issue.body
135+
.replace(/<[^>]*>/g, '') // Remove HTML tags
136+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // Remove markdown links, keep text
137+
.replace(/\s+/g, ' ') // Normalize whitespace
138+
.trim();
139+
const description = sanitized.substring(0, 150);
140+
comment += ` ${description}${sanitized.length > 150 ? '...' : ''}\n\n`;
141+
} else {
142+
comment += ` *No description available*\n\n`;
143+
}
144+
});
145+
} else {
146+
comment += `There are currently no open ${wasGoodFirstIssue ? 'beginner' : 'similar'} issues in this repository. \n\n`;
147+
comment += `You can check out good first issues across the entire Hiero organization: [Hiero Good First Issues](https://github.com/issues?q=is%3Aopen+is%3Aissue+org%3Ahiero-ledger+archived%3Afalse+label%3A%22good+first+issue%22+)\n\n`;
148+
}
149+
150+
comment += `🌟 **Stay connected with the project:**\n`;
151+
comment += `- ⭐ [Star this repository](https://github.com/${context.repo.owner}/${context.repo.repo}) to show your support\n`;
152+
comment += `- 👀 [Watch this repository](https://github.com/${context.repo.owner}/${context.repo.repo}/watchers) to get notified of new issues and releases\n\n`;
153+
154+
comment += `We look forward to seeing more contributions from you! If you have any questions, feel free to ask in our [Discord community](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md).\n\n`;
155+
comment += `From the Hiero Python SDK Team 🚀`;
156+
157+
// Check for existing comment
158+
try {
159+
const { data: comments } = await github.rest.issues.listComments({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
issue_number: prNumber,
163+
});
164+
165+
const existingComment = comments.find(comment => comment.body.includes(marker));
166+
167+
if (existingComment) {
168+
core.info('Comment already exists, skipping');
169+
return;
170+
}
171+
} catch (error) {
172+
core.warning(`Error checking existing comments: ${error.message}`);
173+
}
174+
175+
// Post the comment
176+
try {
177+
await github.rest.issues.createComment({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
issue_number: prNumber,
181+
body: comment,
182+
});
183+
184+
core.info(`Successfully posted comment to PR #${prNumber}`);
185+
} catch (error) {
186+
core.setFailed(`Error posting comment: ${error.message}`);
187+
}
188+
}

0 commit comments

Comments
 (0)