Skip to content
Merged
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 { 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 @@ -140,7 +140,7 @@
async function main() {
const reaction = process.env.GH_AW_REACTION || "eyes";
const commandsJSON = process.env.GH_AW_COMMANDS;
const command = commandsJSON ? (JSON.parse(commandsJSON)[0] ?? null) : null; // Only present for command workflows

Check warning on line 143 in actions/setup/js/add_reaction_and_edit_comment.cjs

View workflow job for this annotation

GitHub Actions / lint-js

Wrap JSON.parse(commandsJSON) in try/catch to avoid uncaught runtime failures in actions/setup/js
const invocationContext = resolveInvocationContext(context);
const runUrl = buildWorkflowRunUrl(context, invocationContext.workflowRepo);

Expand Down Expand Up @@ -250,19 +250,7 @@
// 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
54 changes: 54 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 @@ -418,4 +420,56 @@ describe("github_api_helpers.cjs", () => {
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.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);

await expect(createDiscussionComment({ graphql: mockGraphql }, "D_1", "hello")).rejects.toThrow("GraphQL failed");
});
});
});
Loading