Skip to content

Commit aa2b202

Browse files
authored
fix : postComment Duplicate issue (#2578)
Signed-off-by: anchit-goel <anchitgoel5@gmail.com>
1 parent 778c0e1 commit aa2b202

3 files changed

Lines changed: 181 additions & 4 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// __tests__/jest/post-comment.test.js
2+
//
3+
// Run from .github/scripts:
4+
// npm run test:js -- post-comment.test.js
5+
6+
const { postComment } = require('../../shared/helpers/pr-helpers');
7+
8+
function createMockGithub({ shouldFail = false, errorMessage = 'API error' } = {}) {
9+
return {
10+
rest: {
11+
issues: {
12+
createComment: jest.fn(async () => {
13+
if (shouldFail) {
14+
throw new Error(errorMessage);
15+
}
16+
return { data: { id: 101 } };
17+
}),
18+
},
19+
},
20+
};
21+
}
22+
23+
function createMockCore() {
24+
return {
25+
info: jest.fn(),
26+
error: jest.fn(),
27+
warning: jest.fn(),
28+
setFailed: jest.fn(),
29+
};
30+
}
31+
32+
describe('postComment helper', () => {
33+
test('happy path: returns true and logs info when createComment resolves', async () => {
34+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
35+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
36+
37+
try {
38+
const github = createMockGithub();
39+
const core = createMockCore();
40+
const owner = 'hiero-ledger';
41+
const repo = 'hiero-sdk-python';
42+
const prNumber = 123;
43+
const body = 'Great work on this PR!';
44+
45+
const result = await postComment(github, owner, repo, prNumber, body, core);
46+
47+
expect(result).toBe(true);
48+
expect(github.rest.issues.createComment).toHaveBeenCalledTimes(1);
49+
expect(github.rest.issues.createComment).toHaveBeenCalledWith({
50+
owner,
51+
repo,
52+
issue_number: prNumber,
53+
body,
54+
});
55+
expect(core.info).toHaveBeenCalledTimes(1);
56+
expect(core.info).toHaveBeenCalledWith(`Posted recommendation comment to PR #${prNumber}`);
57+
expect(core.error).not.toHaveBeenCalled();
58+
expect(consoleLogSpy).not.toHaveBeenCalled();
59+
expect(consoleErrorSpy).not.toHaveBeenCalled();
60+
} finally {
61+
consoleLogSpy.mockRestore();
62+
consoleErrorSpy.mockRestore();
63+
}
64+
});
65+
66+
test('failure path: returns false, logs error, and does not throw when createComment rejects', async () => {
67+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
68+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
69+
70+
try {
71+
const errorMessage = 'Network error: 500 Internal Server Error';
72+
const github = createMockGithub({ shouldFail: true, errorMessage });
73+
const core = createMockCore();
74+
const owner = 'hiero-ledger';
75+
const repo = 'hiero-sdk-python';
76+
const prNumber = 456;
77+
const body = 'Recommendation comment';
78+
79+
let result;
80+
let thrownError = null;
81+
82+
try {
83+
result = await postComment(github, owner, repo, prNumber, body, core);
84+
} catch (err) {
85+
thrownError = err;
86+
}
87+
88+
expect(thrownError).toBeNull();
89+
expect(result).toBe(false);
90+
expect(github.rest.issues.createComment).toHaveBeenCalledTimes(1);
91+
expect(github.rest.issues.createComment).toHaveBeenCalledWith({
92+
owner,
93+
repo,
94+
issue_number: prNumber,
95+
body,
96+
});
97+
expect(core.error).toHaveBeenCalledTimes(1);
98+
expect(core.error).toHaveBeenCalledWith(`Failed to post comment: ${errorMessage}`);
99+
expect(core.info).not.toHaveBeenCalled();
100+
expect(consoleLogSpy).not.toHaveBeenCalled();
101+
expect(consoleErrorSpy).not.toHaveBeenCalled();
102+
} finally {
103+
consoleLogSpy.mockRestore();
104+
consoleErrorSpy.mockRestore();
105+
}
106+
});
107+
});

.github/scripts/shared/api/github-api.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ async function fetchIssuesBatch(github, repoConfig) {
6565
}
6666
}
6767

68+
/**
69+
* Returns the number of open issues currently assigned to `username` in the
70+
* given repository. Pull requests are excluded because they do not consume
71+
* assignment capacity. Returns null on API failure so callers can fail open.
72+
*
73+
* @param {object} params
74+
* @param {import('@actions/github').GitHub} params.github
75+
* @param {string} params.owner - Repo owner.
76+
* @param {string} params.repo - Repo name.
77+
* @param {string} params.username - GitHub login of the contributor.
78+
* @returns {Promise<number|null>} Count of open issue assignments, or null on failure.
79+
*/
6880
async function getOpenAssignments({ github, owner, repo, username }) {
6981
try {
7082
const issues = await github.paginate(github.rest.issues.listForRepo, {
@@ -94,6 +106,14 @@ async function getOpenAssignments({ github, owner, repo, username }) {
94106
* Counts closed issues carrying `label` (in the given repo) assigned to
95107
* `username`. Returns null (rather than throwing) on unsafe input or API
96108
* error so callers can choose to fail open.
109+
*
110+
* @param {object} params
111+
* @param {import('@actions/github').GitHub} params.github
112+
* @param {string} params.owner - Repo owner.
113+
* @param {string} params.repo - Repo name.
114+
* @param {string} params.username - GitHub login of the contributor.
115+
* @param {string} params.label - Label string to filter by.
116+
* @returns {Promise<number|null>} Issue count, or null on invalid input or API failure.
97117
*/
98118
async function countCompletedIssuesWithLabel({ github, owner, repo, username, label }) {
99119
if (!isValidSearchToken(owner) || !isValidSearchToken(repo) || !isValidSearchToken(username)) {
@@ -133,12 +153,18 @@ async function countCompletedIssuesWithLabel({ github, owner, repo, username, la
133153
}
134154

135155
/**
136-
137156
* Determines whether a user has repository collaborator access.
138157
*
139158
* Repository owners are always considered collaborators.
140159
* GitHub returns 204 when the user is a collaborator and 404 otherwise.
141160
* Unexpected API failures are treated as non-collaborator access.
161+
*
162+
* @param {object} params
163+
* @param {import('@actions/github').GitHub} params.github
164+
* @param {string} params.owner - Repo owner.
165+
* @param {string} params.repo - Repo name.
166+
* @param {string} params.username - GitHub login to check.
167+
* @returns {Promise<boolean>} True if the user is a collaborator, false otherwise.
142168
*/
143169
async function isRepoCollaborator({ github, owner, repo, username }) {
144170
if (username === owner) {
@@ -176,16 +202,46 @@ async function isRepoCollaborator({ github, owner, repo, username }) {
176202
}
177203
}
178204

205+
/**
206+
* Posts a comment on an issue or pull request via the GitHub REST API.
207+
* When `logLabel` is provided, logs the outcome to the console; when omitted,
208+
* stays silent so the caller owns all logging. Re-throws any API error either
209+
* way so callers can handle or propagate failures themselves.
210+
*
211+
* @param {object} params
212+
* @param {import('@actions/github').GitHub} params.github
213+
* @param {string} params.owner - Repo owner.
214+
* @param {string} params.repo - Repo name.
215+
* @param {number} params.issueNumber - Issue or PR number to comment on.
216+
* @param {string} params.body - Markdown body of the comment.
217+
* @param {string} [logLabel] - Optional human-readable label used in console output.
218+
* @returns {Promise<void>}
219+
* @throws {Error} Re-throws the Octokit error on API failure.
220+
*/
179221
async function postIssueComment({ github, owner, repo, issueNumber, body }, logLabel) {
180222
try {
181223
await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body });
182-
console.log(`[github-api] Posted comment: ${logLabel}`);
224+
if (logLabel) {
225+
console.log(`[github-api] Posted comment: ${logLabel}`);
226+
}
183227
} catch (error) {
184-
console.error(`[github-api] Failed to post comment (${logLabel}):`, { message: error.message });
228+
if (logLabel) {
229+
console.error(`[github-api] Failed to post comment (${logLabel}):`, { message: error.message });
230+
}
185231
throw error;
186232
}
187233
}
188234

235+
/**
236+
* Fetches all comments on an issue or pull request, paginating automatically.
237+
*
238+
* @param {object} params
239+
* @param {import('@actions/github').GitHub} params.github
240+
* @param {string} params.owner - Repo owner.
241+
* @param {string} params.repo - Repo name.
242+
* @param {number} params.issueNumber - Issue or PR number.
243+
* @returns {Promise<Array<object>>} Array of comment objects from the GitHub API.
244+
*/
189245
async function fetchAllComments({ github, owner, repo, issueNumber }) {
190246
return github.paginate(github.rest.issues.listComments, {
191247
owner,
@@ -195,6 +251,17 @@ async function fetchAllComments({ github, owner, repo, issueNumber }) {
195251
});
196252
}
197253

254+
/**
255+
* Assigns a contributor to an issue.
256+
*
257+
* @param {object} params
258+
* @param {import('@actions/github').GitHub} params.github
259+
* @param {string} params.owner - Repo owner.
260+
* @param {string} params.repo - Repo name.
261+
* @param {number} params.issueNumber - Issue number to assign.
262+
* @param {string} params.username - GitHub login of the contributor to assign.
263+
* @returns {Promise<void>}
264+
*/
198265
async function assignIssue({
199266
github,
200267
owner,

.github/scripts/shared/helpers/pr-helpers.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ---------------------------------------------------------------------------
44

55
const { CONFIG } = require('../config');
6+
const { postIssueComment } = require('../api/github-api');
67

78
/**
89
* Extracts the first linked issue number from a PR body using closing keywords.
@@ -71,7 +72,9 @@ async function alreadyCommented(github, owner, repo, prNumber) {
7172
*/
7273
async function postComment(github, owner, repo, prNumber, body, core) {
7374
try {
74-
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
75+
// No logLabel: this wrapper owns logging (via core), matching the
76+
// pre-consolidation log output exactly.
77+
await postIssueComment({ github, owner, repo, issueNumber: prNumber, body });
7578
core.info(`Posted recommendation comment to PR #${prNumber}`);
7679
return true;
7780
} catch (error) {

0 commit comments

Comments
 (0)