Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 2 additions & 28 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 2 additions & 14 deletions actions/setup/js/add_reaction_and_edit_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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;
}
Expand Down
37 changes: 37 additions & 0 deletions actions/setup/js/github_api_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions actions/setup/js/github_api_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe("github_api_helpers.cjs", () => {
let logGraphQLError;
let fetchAllRepoLabels;
let resolveTopLevelDiscussionCommentId;
let createDiscussionComment;
let mockGithub;

beforeEach(async () => {
Expand All @@ -30,6 +31,7 @@ describe("github_api_helpers.cjs", () => {
logGraphQLError = module.logGraphQLError;
fetchAllRepoLabels = module.fetchAllRepoLabels;
resolveTopLevelDiscussionCommentId = module.resolveTopLevelDiscussionCommentId;
createDiscussionComment = module.createDiscussionComment;
});

describe("getFileContent", () => {
Expand Down Expand Up @@ -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");
});
});
});
});