From 2565335cd08904d39f12d427577a6a85c9ca6c33 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 04:54:24 +0000 Subject: [PATCH 1/2] jsweep: clean add_reaction_and_edit_comment.cjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract large switch statement into resolveEventEndpoints() helper - Move validReactions to module-level VALID_REACTIONS constant - Remove redundant if (commentUpdateEndpoint) guard after switch - Add explicit Record type for EVENT_TYPE_DESCRIPTIONS - Export resolveEventEndpoints and VALID_REACTIONS for direct testing - Add 9 new tests for resolveEventEndpoints and VALID_REACTIONS (35 → 44) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../js/add_reaction_and_edit_comment.cjs | 227 ++++++++++-------- .../js/add_reaction_and_edit_comment.test.cjs | 88 ++++++- 2 files changed, 209 insertions(+), 106 deletions(-) diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index 92dd2f9e11e..012019ca22a 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -13,6 +13,7 @@ const { addReaction, addDiscussionReaction } = require("./add_reaction.cjs"); /** * Event type descriptions for comment messages + * @type {Record} */ const EVENT_TYPE_DESCRIPTIONS = { issues: "issue", @@ -23,6 +24,119 @@ const EVENT_TYPE_DESCRIPTIONS = { discussion_comment: "discussion comment", }; +/** Valid GitHub reaction types */ +const VALID_REACTIONS = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]; + +/** + * Resolve the reaction and comment API endpoints for a given event. + * Returns null (after calling core.setFailed) when the event or payload is invalid. + * @param {string} eventName - The GitHub event name + * @param {string} owner - Repository owner + * @param {string} repo - Repository name + * @param {Record} payload - The event payload + * @returns {Promise<{reactionEndpoint: string, commentUpdateEndpoint: string} | null>} + */ +async function resolveEventEndpoints(eventName, owner, repo, payload) { + switch (eventName) { + case "issues": { + const issueNumber = payload?.issue?.number; + if (!issueNumber) { + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); + return null; + } + return { + reactionEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`, + commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, + }; + } + + case "issue_comment": { + const commentId = payload?.comment?.id; + const issueNumber = payload?.issue?.number; + if (!commentId) { + core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`); + return null; + } + if (!issueNumber) { + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); + return null; + } + return { + reactionEndpoint: `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`, + // Create new comment on the issue itself, not on the comment + commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, + }; + } + + case "pull_request": { + const prNumber = payload?.pull_request?.number; + if (!prNumber) { + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); + return null; + } + // PRs are "issues" for the reactions endpoint + return { + reactionEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/reactions`, + commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/comments`, + }; + } + + case "pull_request_review_comment": { + const reviewCommentId = payload?.comment?.id; + const prNumber = payload?.pull_request?.number; + if (!reviewCommentId) { + core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`); + return null; + } + if (!prNumber) { + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); + return null; + } + return { + reactionEndpoint: `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`, + // Create new comment on the PR itself (using issues endpoint since PRs are issues) + commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/comments`, + }; + } + + case "discussion": { + const discussionNumber = payload?.discussion?.number; + if (!discussionNumber) { + core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); + return null; + } + // Discussions use GraphQL API - get the node ID + const discussion = await getDiscussionId(owner, repo, discussionNumber); + return { + reactionEndpoint: discussion.id, // Store node ID for GraphQL + commentUpdateEndpoint: `discussion:${discussionNumber}`, // Special format to indicate discussion + }; + } + + case "discussion_comment": { + const discussionNumber = payload?.discussion?.number; + const commentId = payload?.comment?.id; + if (!discussionNumber || !commentId) { + core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`); + return null; + } + const commentNodeId = payload?.comment?.node_id; + if (!commentNodeId) { + core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`); + return null; + } + return { + reactionEndpoint: commentNodeId, // Store node ID for GraphQL + commentUpdateEndpoint: `discussion_comment:${discussionNumber}:${commentId}`, // Special format + }; + } + + default: + core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`); + return null; + } +} + async function main() { const reaction = process.env.GH_AW_REACTION || "eyes"; const command = process.env.GH_AW_COMMAND; // Only present for command workflows @@ -34,113 +148,20 @@ async function main() { core.info(`Run ID: ${context.runId}`); core.info(`Run URL: ${runUrl}`); - // Validate reaction type - const validReactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]; - if (!validReactions.includes(reaction)) { - core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); + if (!VALID_REACTIONS.includes(reaction)) { + core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${VALID_REACTIONS.join(", ")}`); return; } - let reactionEndpoint; - let commentUpdateEndpoint; const eventName = invocationContext.eventName; - const owner = invocationContext.eventRepo.owner; - const repo = invocationContext.eventRepo.repo; + const { owner, repo } = invocationContext.eventRepo; const payload = invocationContext.eventPayload; try { - switch (eventName) { - case "issues": { - const issueNumber = payload?.issue?.number; - if (!issueNumber) { - core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); - return; - } - reactionEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`; - commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/comments`; - break; - } - - case "issue_comment": { - const commentId = payload?.comment?.id; - const issueNumberForComment = payload?.issue?.number; - if (!commentId) { - core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`); - return; - } - if (!issueNumberForComment) { - core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); - return; - } - reactionEndpoint = `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`; - // Create new comment on the issue itself, not on the comment - commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${issueNumberForComment}/comments`; - break; - } + const endpoints = await resolveEventEndpoints(eventName, owner, repo, payload); + if (!endpoints) return; - case "pull_request": { - const prNumber = payload?.pull_request?.number; - if (!prNumber) { - core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); - return; - } - // PRs are "issues" for the reactions endpoint - reactionEndpoint = `/repos/${owner}/${repo}/issues/${prNumber}/reactions`; - commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${prNumber}/comments`; - break; - } - - case "pull_request_review_comment": { - const reviewCommentId = payload?.comment?.id; - const prNumberForReviewComment = payload?.pull_request?.number; - if (!reviewCommentId) { - core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`); - return; - } - if (!prNumberForReviewComment) { - core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); - return; - } - reactionEndpoint = `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`; - // Create new comment on the PR itself (using issues endpoint since PRs are issues) - commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${prNumberForReviewComment}/comments`; - break; - } - - case "discussion": { - const discussionNumber = payload?.discussion?.number; - if (!discussionNumber) { - core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); - return; - } - // Discussions use GraphQL API - get the node ID - const discussion = await getDiscussionId(owner, repo, discussionNumber); - reactionEndpoint = discussion.id; // Store node ID for GraphQL - commentUpdateEndpoint = `discussion:${discussionNumber}`; // Special format to indicate discussion - break; - } - - case "discussion_comment": { - const discussionCommentNumber = payload?.discussion?.number; - const discussionCommentId = payload?.comment?.id; - if (!discussionCommentNumber || !discussionCommentId) { - core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`); - return; - } - const commentNodeId = payload?.comment?.node_id; - if (!commentNodeId) { - core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`); - return; - } - reactionEndpoint = commentNodeId; // Store node ID for GraphQL - commentUpdateEndpoint = `discussion_comment:${discussionCommentNumber}:${discussionCommentId}`; // Special format - break; - } - - default: - core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`); - return; - } + const { reactionEndpoint, commentUpdateEndpoint } = endpoints; core.info(`Reaction API endpoint: ${reactionEndpoint}`); @@ -151,10 +172,8 @@ async function main() { await addReaction(reactionEndpoint, reaction); } - if (commentUpdateEndpoint) { - core.info(`Comment endpoint: ${commentUpdateEndpoint}`); - await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName, invocationContext); - } + core.info(`Comment endpoint: ${commentUpdateEndpoint}`); + await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName, invocationContext); } catch (error) { if (isLockedError(error)) { core.info(`Cannot add reaction: resource is locked (this is expected and not an error)`); @@ -319,4 +338,4 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio } } -module.exports = { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction }; +module.exports = { main, addCommentWithWorkflowLink, resolveEventEndpoints, VALID_REACTIONS, addReaction, addDiscussionReaction }; diff --git a/actions/setup/js/add_reaction_and_edit_comment.test.cjs b/actions/setup/js/add_reaction_and_edit_comment.test.cjs index c7691f1c600..d19c6f85697 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.test.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.test.cjs @@ -38,8 +38,8 @@ global.context = mockContext; // Helper to import the module fresh (bust module cache) async function loadModule() { - const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now()); - return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction }; + const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now()); + return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS }; } describe("add_reaction_and_edit_comment.cjs", () => { @@ -609,4 +609,88 @@ describe("add_reaction_and_edit_comment.cjs", () => { expect(mockCore.setOutput).toHaveBeenCalledWith("reaction-id", ""); }); }); + + describe("VALID_REACTIONS", () => { + it("should export the list of valid reaction types", async () => { + const { VALID_REACTIONS } = await loadModule(); + expect(VALID_REACTIONS).toEqual(["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]); + }); + }); + + describe("resolveEventEndpoints()", () => { + it("should resolve endpoints for issues event", async () => { + const { resolveEventEndpoints } = await loadModule(); + const payload = { issue: { number: 42 } }; + const result = await resolveEventEndpoints("issues", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "/repos/owner/repo/issues/42/reactions", + commentUpdateEndpoint: "/repos/owner/repo/issues/42/comments", + }); + }); + + it("should return null and call setFailed when issue number is missing", async () => { + const { resolveEventEndpoints } = await loadModule(); + const result = await resolveEventEndpoints("issues", "owner", "repo", {}); + expect(result).toBeNull(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_NOT_FOUND)); + }); + + it("should resolve endpoints for pull_request event", async () => { + const { resolveEventEndpoints } = await loadModule(); + const payload = { pull_request: { number: 7 } }; + const result = await resolveEventEndpoints("pull_request", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "/repos/owner/repo/issues/7/reactions", + commentUpdateEndpoint: "/repos/owner/repo/issues/7/comments", + }); + }); + + it("should resolve endpoints for issue_comment event", async () => { + const { resolveEventEndpoints } = await loadModule(); + const payload = { comment: { id: 55 }, issue: { number: 10 } }; + const result = await resolveEventEndpoints("issue_comment", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "/repos/owner/repo/issues/comments/55/reactions", + commentUpdateEndpoint: "/repos/owner/repo/issues/10/comments", + }); + }); + + it("should resolve endpoints for pull_request_review_comment event", async () => { + const { resolveEventEndpoints } = await loadModule(); + const payload = { comment: { id: 99 }, pull_request: { number: 3 } }; + const result = await resolveEventEndpoints("pull_request_review_comment", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "/repos/owner/repo/pulls/comments/99/reactions", + commentUpdateEndpoint: "/repos/owner/repo/issues/3/comments", + }); + }); + + it("should resolve endpoints for discussion event using GraphQL node ID", async () => { + mockGithub.graphql.mockResolvedValueOnce({ repository: { discussion: { id: "D_node123", url: "https://github.com/testowner/testrepo/discussions/5" } } }); + const { resolveEventEndpoints } = await loadModule(); + const payload = { discussion: { number: 5 } }; + const result = await resolveEventEndpoints("discussion", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "D_node123", + commentUpdateEndpoint: "discussion:5", + }); + }); + + it("should resolve endpoints for discussion_comment event", async () => { + const { resolveEventEndpoints } = await loadModule(); + const payload = { discussion: { number: 5 }, comment: { id: 88, node_id: "DC_node88" } }; + const result = await resolveEventEndpoints("discussion_comment", "owner", "repo", payload); + expect(result).toEqual({ + reactionEndpoint: "DC_node88", + commentUpdateEndpoint: "discussion_comment:5:88", + }); + }); + + it("should return null and call setFailed for unknown event type", async () => { + const { resolveEventEndpoints } = await loadModule(); + const result = await resolveEventEndpoints("push", "owner", "repo", {}); + expect(result).toBeNull(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_VALIDATION)); + }); + }); }); From 5f1995f2f72936f2b25e875239ad460e6764d113 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 17:45:52 +0000 Subject: [PATCH 2/2] freeze VALID_REACTIONS array with Object.freeze() Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fb12d090-f298-4a07-b4d3-b03e8e2d7d6a Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/add_reaction_and_edit_comment.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index 012019ca22a..450941699a7 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -25,7 +25,7 @@ const EVENT_TYPE_DESCRIPTIONS = { }; /** Valid GitHub reaction types */ -const VALID_REACTIONS = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]; +const VALID_REACTIONS = Object.freeze(["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]); /** * Resolve the reaction and comment API endpoints for a given event.