Skip to content

Commit 22976b0

Browse files
authored
Deduplicate discussion comment mutation flow across comment actions (#43482)
1 parent fdb9beb commit 22976b0

4 files changed

Lines changed: 95 additions & 42 deletions

File tree

actions/setup/js/add_comment.cjs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const { getMessages } = require("./messages_core.cjs");
2020
const { getBodyHeader } = require("./messages_header.cjs");
2121
const { sanitizeContent } = require("./sanitize_content.cjs");
2222
const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs");
23-
const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
23+
const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
2424
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
2525
const { ERR_NOT_FOUND } = require("./error_codes.cjs");
2626
const { isPayloadUserBot } = require("./resolve_mentions.cjs");
@@ -379,33 +379,7 @@ async function commentOnDiscussion(github, owner, repo, discussionNumber, messag
379379
const discussionUrl = repository.discussion.url;
380380

381381
// 2. Add comment (with optional replyToId for threading)
382-
const mutation = replyToId
383-
? /* GraphQL */ `
384-
mutation ($dId: ID!, $body: String!, $replyToId: ID!) {
385-
addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) {
386-
comment {
387-
id
388-
url
389-
}
390-
}
391-
}
392-
`
393-
: /* GraphQL */ `
394-
mutation ($dId: ID!, $body: String!) {
395-
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
396-
comment {
397-
id
398-
url
399-
}
400-
}
401-
}
402-
`;
403-
404-
const variables = { dId: discussionId, body: message, ...(replyToId ? { replyToId } : {}) };
405-
406-
const result = await github.graphql(mutation, variables);
407-
408-
const comment = result.addDiscussionComment.comment;
382+
const comment = await createDiscussionComment(github, discussionId, message, replyToId);
409383

410384
return {
411385
id: comment.id,

actions/setup/js/add_reaction_and_edit_comment.cjs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
77
const { sanitizeContent } = require("./sanitize_content.cjs");
88
const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs");
99
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
10-
const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
10+
const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs");
1111
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");
1212
const { addReaction, addDiscussionReaction, getDiscussionNodeId } = require("./add_reaction.cjs");
1313

@@ -250,19 +250,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio
250250
// For discussion_comment events, thread the reply under the triggering comment.
251251
// GitHub Discussions only supports two nesting levels, so resolve the top-level parent node ID.
252252
const replyToId = eventName === "discussion_comment" ? await resolveTopLevelDiscussionCommentId(github, eventPayload?.comment?.node_id) : null;
253-
const mutation = replyToId
254-
? `mutation($dId: ID!, $body: String!, $replyToId: ID!) {
255-
addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) {
256-
comment { id url }
257-
}
258-
}`
259-
: `mutation($dId: ID!, $body: String!) {
260-
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
261-
comment { id url }
262-
}
263-
}`;
264-
const result = await github.graphql(mutation, { dId: discussionId, body: commentBody, ...(replyToId ? { replyToId } : {}) });
265-
const comment = result.addDiscussionComment.comment;
253+
const comment = await createDiscussionComment(github, discussionId, commentBody, replyToId);
266254
setCommentOutputs(comment.id, comment.url, eventRepo);
267255
return;
268256
}

actions/setup/js/github_api_helpers.cjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,44 @@ async function resolveTopLevelDiscussionCommentId(github, commentNodeId) {
175175
}
176176
}
177177

178+
/**
179+
* Create a discussion comment with optional threaded reply target.
180+
* @param {Object} github - GitHub API client (must support graphql)
181+
* @param {string} discussionId - Discussion GraphQL node ID
182+
* @param {string} body - Comment body
183+
* @param {string|null|undefined} [replyToId] - Optional top-level discussion comment node ID for threading
184+
* @returns {Promise<{id: string, url: string}>}
185+
*/
186+
async function createDiscussionComment(github, discussionId, body, replyToId) {
187+
const mutation = replyToId
188+
? /* GraphQL */ `
189+
mutation ($dId: ID!, $body: String!, $replyToId: ID!) {
190+
addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) {
191+
comment {
192+
id
193+
url
194+
}
195+
}
196+
}
197+
`
198+
: /* GraphQL */ `
199+
mutation ($dId: ID!, $body: String!) {
200+
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
201+
comment {
202+
id
203+
url
204+
}
205+
}
206+
}
207+
`;
208+
209+
const variables = { dId: discussionId, body, ...(replyToId ? { replyToId } : {}) };
210+
const result = await github.graphql(mutation, variables);
211+
return result.addDiscussionComment.comment;
212+
}
213+
178214
module.exports = {
215+
createDiscussionComment,
179216
fetchAllRepoLabels,
180217
getFileContent,
181218
logGraphQLError,

actions/setup/js/github_api_helpers.test.cjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe("github_api_helpers.cjs", () => {
1111
let logGraphQLError;
1212
let fetchAllRepoLabels;
1313
let resolveTopLevelDiscussionCommentId;
14+
let createDiscussionComment;
1415
let mockGithub;
1516

1617
beforeEach(async () => {
@@ -30,6 +31,7 @@ describe("github_api_helpers.cjs", () => {
3031
logGraphQLError = module.logGraphQLError;
3132
fetchAllRepoLabels = module.fetchAllRepoLabels;
3233
resolveTopLevelDiscussionCommentId = module.resolveTopLevelDiscussionCommentId;
34+
createDiscussionComment = module.createDiscussionComment;
3335
});
3436

3537
describe("getFileContent", () => {
@@ -418,4 +420,56 @@ describe("github_api_helpers.cjs", () => {
418420
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resolving top-level discussion comment"));
419421
});
420422
});
423+
424+
describe("createDiscussionComment", () => {
425+
it("should create a top-level discussion comment when replyToId is omitted", async () => {
426+
const mockGraphql = vi.fn().mockResolvedValueOnce({
427+
addDiscussionComment: { comment: { id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" } },
428+
});
429+
430+
const result = await createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello");
431+
432+
expect(result).toEqual({ id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" });
433+
expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), {
434+
dId: "D_1",
435+
body: "hello",
436+
});
437+
expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId");
438+
});
439+
440+
it("should create a threaded discussion comment when replyToId is provided", async () => {
441+
const mockGraphql = vi.fn().mockResolvedValueOnce({
442+
addDiscussionComment: { comment: { id: "DC_2", url: "https://github.com/o/r/discussions/1#discussioncomment-2" } },
443+
});
444+
445+
await createDiscussionComment({ graphql: mockGraphql }, "D_1", "reply", "DC_parent");
446+
447+
expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("$replyToId"), {
448+
dId: "D_1",
449+
body: "reply",
450+
replyToId: "DC_parent",
451+
});
452+
});
453+
454+
it("should treat null replyToId as omitted", async () => {
455+
const mockGraphql = vi.fn().mockResolvedValueOnce({
456+
addDiscussionComment: { comment: { id: "DC_3", url: "https://github.com/o/r/discussions/1#discussioncomment-3" } },
457+
});
458+
459+
await createDiscussionComment({ graphql: mockGraphql }, "D_1", "top-level", null);
460+
461+
expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), {
462+
dId: "D_1",
463+
body: "top-level",
464+
});
465+
expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId");
466+
});
467+
468+
it("should propagate graphql errors", async () => {
469+
const error = new Error("GraphQL failed");
470+
const mockGraphql = vi.fn().mockRejectedValueOnce(error);
471+
472+
await expect(createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello")).rejects.toThrow("GraphQL failed");
473+
});
474+
});
421475
});

0 commit comments

Comments
 (0)