From 0a935213b6322286d59d9bd040546eeae6f6e0c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:33:50 +0000 Subject: [PATCH 1/3] Initial plan From 9b36240c7be18b6b70d8e733fb6388c58d4a4e19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:06:12 +0000 Subject: [PATCH 2/3] Refactor discussion comment mutation into shared helper Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/add_comment.cjs | 30 +---------- .../js/add_reaction_and_edit_comment.cjs | 16 +----- actions/setup/js/github_api_helpers.cjs | 37 +++++++++++++ actions/setup/js/github_api_helpers.test.cjs | 53 +++++++++++++++++++ 4 files changed, 94 insertions(+), 42 deletions(-) diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index 7f85c86c74d..90888bbd240 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -20,7 +20,7 @@ const { getMessages } = require("./messages_core.cjs"); const { getBodyHeader } = require("./messages_header.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs"); -const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs"); +const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { ERR_NOT_FOUND } = require("./error_codes.cjs"); const { isPayloadUserBot } = require("./resolve_mentions.cjs"); @@ -379,33 +379,7 @@ async function commentOnDiscussion(github, owner, repo, discussionNumber, messag const discussionUrl = repository.discussion.url; // 2. Add comment (with optional replyToId for threading) - const mutation = replyToId - ? /* GraphQL */ ` - mutation ($dId: ID!, $body: String!, $replyToId: ID!) { - addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) { - comment { - id - url - } - } - } - ` - : /* GraphQL */ ` - mutation ($dId: ID!, $body: String!) { - addDiscussionComment(input: { discussionId: $dId, body: $body }) { - comment { - id - url - } - } - } - `; - - const variables = { dId: discussionId, body: message, ...(replyToId ? { replyToId } : {}) }; - - const result = await github.graphql(mutation, variables); - - const comment = result.addDiscussionComment.comment; + const comment = await createDiscussionComment(github, discussionId, message, replyToId); return { id: comment.id, diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index fa00fedee60..34a08807637 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -7,7 +7,7 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); -const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs"); +const { createDiscussionComment, resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs"); const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); const { addReaction, addDiscussionReaction, getDiscussionNodeId } = require("./add_reaction.cjs"); @@ -250,19 +250,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio // For discussion_comment events, thread the reply under the triggering comment. // GitHub Discussions only supports two nesting levels, so resolve the top-level parent node ID. const replyToId = eventName === "discussion_comment" ? await resolveTopLevelDiscussionCommentId(github, eventPayload?.comment?.node_id) : null; - const mutation = replyToId - ? `mutation($dId: ID!, $body: String!, $replyToId: ID!) { - addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) { - comment { id url } - } - }` - : `mutation($dId: ID!, $body: String!) { - addDiscussionComment(input: { discussionId: $dId, body: $body }) { - comment { id url } - } - }`; - const result = await github.graphql(mutation, { dId: discussionId, body: commentBody, ...(replyToId ? { replyToId } : {}) }); - const comment = result.addDiscussionComment.comment; + const comment = await createDiscussionComment(github, discussionId, commentBody, replyToId); setCommentOutputs(comment.id, comment.url, eventRepo); return; } diff --git a/actions/setup/js/github_api_helpers.cjs b/actions/setup/js/github_api_helpers.cjs index af324c5ea3e..17fb7a16cd9 100644 --- a/actions/setup/js/github_api_helpers.cjs +++ b/actions/setup/js/github_api_helpers.cjs @@ -175,7 +175,44 @@ async function resolveTopLevelDiscussionCommentId(github, commentNodeId) { } } +/** + * Create a discussion comment with optional threaded reply target. + * @param {Object} github - GitHub API client (must support graphql) + * @param {string} discussionId - Discussion GraphQL node ID + * @param {string} body - Comment body + * @param {string|null|undefined} [replyToId] - Optional top-level discussion comment node ID for threading + * @returns {Promise<{id: string, url: string}>} + */ +async function createDiscussionComment(github, discussionId, body, replyToId) { + const mutation = replyToId + ? /* GraphQL */ ` + mutation ($dId: ID!, $body: String!, $replyToId: ID!) { + addDiscussionComment(input: { discussionId: $dId, body: $body, replyToId: $replyToId }) { + comment { + id + url + } + } + } + ` + : /* GraphQL */ ` + mutation ($dId: ID!, $body: String!) { + addDiscussionComment(input: { discussionId: $dId, body: $body }) { + comment { + id + url + } + } + } + `; + + const variables = { dId: discussionId, body, ...(replyToId ? { replyToId } : {}) }; + const result = await github.graphql(mutation, variables); + return result.addDiscussionComment.comment; +} + module.exports = { + createDiscussionComment, fetchAllRepoLabels, getFileContent, logGraphQLError, diff --git a/actions/setup/js/github_api_helpers.test.cjs b/actions/setup/js/github_api_helpers.test.cjs index ac1f571f3af..620bb0c998d 100644 --- a/actions/setup/js/github_api_helpers.test.cjs +++ b/actions/setup/js/github_api_helpers.test.cjs @@ -11,6 +11,7 @@ describe("github_api_helpers.cjs", () => { let logGraphQLError; let fetchAllRepoLabels; let resolveTopLevelDiscussionCommentId; + let createDiscussionComment; let mockGithub; beforeEach(async () => { @@ -30,6 +31,7 @@ describe("github_api_helpers.cjs", () => { logGraphQLError = module.logGraphQLError; fetchAllRepoLabels = module.fetchAllRepoLabels; resolveTopLevelDiscussionCommentId = module.resolveTopLevelDiscussionCommentId; + createDiscussionComment = module.createDiscussionComment; }); describe("getFileContent", () => { @@ -417,5 +419,56 @@ describe("github_api_helpers.cjs", () => { expect(result).toBe("DC_fallback123"); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resolving top-level discussion comment")); }); + + describe("createDiscussionComment", () => { + it("should create a top-level discussion comment when replyToId is omitted", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" } }, + }); + + const result = await createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello"); + + expect(result).toEqual({ id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" }); + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), { + dId: "D_1", + body: "hello", + }); + expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId"); + }); + + it("should create a threaded discussion comment when replyToId is provided", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_2", url: "https://github.com/o/r/discussions/1#discussioncomment-2" } }, + }); + + await createDiscussionComment({ graphql: mockGraphql }, "D_1", "reply", "DC_parent"); + + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("$replyToId"), { + dId: "D_1", + body: "reply", + replyToId: "DC_parent", + }); + }); + + it("should treat null replyToId as omitted", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_3", url: "https://github.com/o/r/discussions/1#discussioncomment-3" } }, + }); + + await createDiscussionComment({ graphql: mockGraphql }, "D_1", "top-level", null); + + expect(mockGraphql).toHaveBeenCalledWith(expect.not.stringContaining("$replyToId"), { + dId: "D_1", + body: "top-level", + }); + }); + + it("should propagate graphql errors", async () => { + const error = new Error("GraphQL failed"); + const mockGraphql = vi.fn().mockRejectedValueOnce(error); + + await expect(createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello")).rejects.toThrow("GraphQL failed"); + }); + }); }); }); From 7466cd0623ed3b3db9c1a191176ef7dab751250b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:08:39 +0000 Subject: [PATCH 3/3] Finalize PR finisher run Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/github_api_helpers.test.cjs | 73 ++++++++++---------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/actions/setup/js/github_api_helpers.test.cjs b/actions/setup/js/github_api_helpers.test.cjs index 620bb0c998d..0e4932d0f10 100644 --- a/actions/setup/js/github_api_helpers.test.cjs +++ b/actions/setup/js/github_api_helpers.test.cjs @@ -419,56 +419,57 @@ describe("github_api_helpers.cjs", () => { expect(result).toBe("DC_fallback123"); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("resolving top-level discussion comment")); }); + }); - describe("createDiscussionComment", () => { - it("should create a top-level discussion comment when replyToId is omitted", async () => { - const mockGraphql = vi.fn().mockResolvedValueOnce({ - addDiscussionComment: { comment: { id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" } }, - }); + describe("createDiscussionComment", () => { + it("should create a top-level discussion comment when replyToId is omitted", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" } }, + }); - const result = await createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello"); + const result = await createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello"); - expect(result).toEqual({ id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" }); - expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), { - dId: "D_1", - body: "hello", - }); - expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId"); + expect(result).toEqual({ id: "DC_1", url: "https://github.com/o/r/discussions/1#discussioncomment-1" }); + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), { + dId: "D_1", + body: "hello", }); + expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId"); + }); - it("should create a threaded discussion comment when replyToId is provided", async () => { - const mockGraphql = vi.fn().mockResolvedValueOnce({ - addDiscussionComment: { comment: { id: "DC_2", url: "https://github.com/o/r/discussions/1#discussioncomment-2" } }, - }); + it("should create a threaded discussion comment when replyToId is provided", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_2", url: "https://github.com/o/r/discussions/1#discussioncomment-2" } }, + }); - await createDiscussionComment({ graphql: mockGraphql }, "D_1", "reply", "DC_parent"); + await createDiscussionComment({ graphql: mockGraphql }, "D_1", "reply", "DC_parent"); - expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("$replyToId"), { - dId: "D_1", - body: "reply", - replyToId: "DC_parent", - }); + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("$replyToId"), { + dId: "D_1", + body: "reply", + replyToId: "DC_parent", }); + }); - it("should treat null replyToId as omitted", async () => { - const mockGraphql = vi.fn().mockResolvedValueOnce({ - addDiscussionComment: { comment: { id: "DC_3", url: "https://github.com/o/r/discussions/1#discussioncomment-3" } }, - }); + it("should treat null replyToId as omitted", async () => { + const mockGraphql = vi.fn().mockResolvedValueOnce({ + addDiscussionComment: { comment: { id: "DC_3", url: "https://github.com/o/r/discussions/1#discussioncomment-3" } }, + }); - await createDiscussionComment({ graphql: mockGraphql }, "D_1", "top-level", null); + await createDiscussionComment({ graphql: mockGraphql }, "D_1", "top-level", null); - expect(mockGraphql).toHaveBeenCalledWith(expect.not.stringContaining("$replyToId"), { - dId: "D_1", - body: "top-level", - }); + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("addDiscussionComment"), { + dId: "D_1", + body: "top-level", }); + expect(String(mockGraphql.mock.calls[0][0])).not.toContain("$replyToId"); + }); - it("should propagate graphql errors", async () => { - const error = new Error("GraphQL failed"); - const mockGraphql = vi.fn().mockRejectedValueOnce(error); + it("should propagate graphql errors", async () => { + const error = new Error("GraphQL failed"); + const mockGraphql = vi.fn().mockRejectedValueOnce(error); - await expect(createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello")).rejects.toThrow("GraphQL failed"); - }); + await expect(createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello")).rejects.toThrow("GraphQL failed"); }); }); });