Skip to content

Commit 6eeb1f7

Browse files
github-actions[bot]CopilotCopilotpelikhan
authored
[jsweep] Clean add_reaction_and_edit_comment.cjs (#30756)
* jsweep: clean add_reaction_and_edit_comment.cjs - 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<string, string> 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> * 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> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 0f8d794 commit 6eeb1f7

2 files changed

Lines changed: 209 additions & 106 deletions

File tree

actions/setup/js/add_reaction_and_edit_comment.cjs

Lines changed: 123 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { addReaction, addDiscussionReaction } = require("./add_reaction.cjs");
1313

1414
/**
1515
* Event type descriptions for comment messages
16+
* @type {Record<string, string>}
1617
*/
1718
const EVENT_TYPE_DESCRIPTIONS = {
1819
issues: "issue",
@@ -23,6 +24,119 @@ const EVENT_TYPE_DESCRIPTIONS = {
2324
discussion_comment: "discussion comment",
2425
};
2526

27+
/** Valid GitHub reaction types */
28+
const VALID_REACTIONS = Object.freeze(["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]);
29+
30+
/**
31+
* Resolve the reaction and comment API endpoints for a given event.
32+
* Returns null (after calling core.setFailed) when the event or payload is invalid.
33+
* @param {string} eventName - The GitHub event name
34+
* @param {string} owner - Repository owner
35+
* @param {string} repo - Repository name
36+
* @param {Record<string, any>} payload - The event payload
37+
* @returns {Promise<{reactionEndpoint: string, commentUpdateEndpoint: string} | null>}
38+
*/
39+
async function resolveEventEndpoints(eventName, owner, repo, payload) {
40+
switch (eventName) {
41+
case "issues": {
42+
const issueNumber = payload?.issue?.number;
43+
if (!issueNumber) {
44+
core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`);
45+
return null;
46+
}
47+
return {
48+
reactionEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`,
49+
commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
50+
};
51+
}
52+
53+
case "issue_comment": {
54+
const commentId = payload?.comment?.id;
55+
const issueNumber = payload?.issue?.number;
56+
if (!commentId) {
57+
core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`);
58+
return null;
59+
}
60+
if (!issueNumber) {
61+
core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`);
62+
return null;
63+
}
64+
return {
65+
reactionEndpoint: `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`,
66+
// Create new comment on the issue itself, not on the comment
67+
commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
68+
};
69+
}
70+
71+
case "pull_request": {
72+
const prNumber = payload?.pull_request?.number;
73+
if (!prNumber) {
74+
core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`);
75+
return null;
76+
}
77+
// PRs are "issues" for the reactions endpoint
78+
return {
79+
reactionEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/reactions`,
80+
commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/comments`,
81+
};
82+
}
83+
84+
case "pull_request_review_comment": {
85+
const reviewCommentId = payload?.comment?.id;
86+
const prNumber = payload?.pull_request?.number;
87+
if (!reviewCommentId) {
88+
core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`);
89+
return null;
90+
}
91+
if (!prNumber) {
92+
core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`);
93+
return null;
94+
}
95+
return {
96+
reactionEndpoint: `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`,
97+
// Create new comment on the PR itself (using issues endpoint since PRs are issues)
98+
commentUpdateEndpoint: `/repos/${owner}/${repo}/issues/${prNumber}/comments`,
99+
};
100+
}
101+
102+
case "discussion": {
103+
const discussionNumber = payload?.discussion?.number;
104+
if (!discussionNumber) {
105+
core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`);
106+
return null;
107+
}
108+
// Discussions use GraphQL API - get the node ID
109+
const discussion = await getDiscussionId(owner, repo, discussionNumber);
110+
return {
111+
reactionEndpoint: discussion.id, // Store node ID for GraphQL
112+
commentUpdateEndpoint: `discussion:${discussionNumber}`, // Special format to indicate discussion
113+
};
114+
}
115+
116+
case "discussion_comment": {
117+
const discussionNumber = payload?.discussion?.number;
118+
const commentId = payload?.comment?.id;
119+
if (!discussionNumber || !commentId) {
120+
core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`);
121+
return null;
122+
}
123+
const commentNodeId = payload?.comment?.node_id;
124+
if (!commentNodeId) {
125+
core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`);
126+
return null;
127+
}
128+
return {
129+
reactionEndpoint: commentNodeId, // Store node ID for GraphQL
130+
commentUpdateEndpoint: `discussion_comment:${discussionNumber}:${commentId}`, // Special format
131+
};
132+
}
133+
134+
default:
135+
core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`);
136+
return null;
137+
}
138+
}
139+
26140
async function main() {
27141
const reaction = process.env.GH_AW_REACTION || "eyes";
28142
const command = process.env.GH_AW_COMMAND; // Only present for command workflows
@@ -34,113 +148,20 @@ async function main() {
34148
core.info(`Run ID: ${context.runId}`);
35149
core.info(`Run URL: ${runUrl}`);
36150

37-
// Validate reaction type
38-
const validReactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"];
39-
if (!validReactions.includes(reaction)) {
40-
core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`);
151+
if (!VALID_REACTIONS.includes(reaction)) {
152+
core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${VALID_REACTIONS.join(", ")}`);
41153
return;
42154
}
43155

44-
let reactionEndpoint;
45-
let commentUpdateEndpoint;
46156
const eventName = invocationContext.eventName;
47-
const owner = invocationContext.eventRepo.owner;
48-
const repo = invocationContext.eventRepo.repo;
157+
const { owner, repo } = invocationContext.eventRepo;
49158
const payload = invocationContext.eventPayload;
50159

51160
try {
52-
switch (eventName) {
53-
case "issues": {
54-
const issueNumber = payload?.issue?.number;
55-
if (!issueNumber) {
56-
core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`);
57-
return;
58-
}
59-
reactionEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`;
60-
commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/comments`;
61-
break;
62-
}
63-
64-
case "issue_comment": {
65-
const commentId = payload?.comment?.id;
66-
const issueNumberForComment = payload?.issue?.number;
67-
if (!commentId) {
68-
core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`);
69-
return;
70-
}
71-
if (!issueNumberForComment) {
72-
core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`);
73-
return;
74-
}
75-
reactionEndpoint = `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`;
76-
// Create new comment on the issue itself, not on the comment
77-
commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${issueNumberForComment}/comments`;
78-
break;
79-
}
161+
const endpoints = await resolveEventEndpoints(eventName, owner, repo, payload);
162+
if (!endpoints) return;
80163

81-
case "pull_request": {
82-
const prNumber = payload?.pull_request?.number;
83-
if (!prNumber) {
84-
core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`);
85-
return;
86-
}
87-
// PRs are "issues" for the reactions endpoint
88-
reactionEndpoint = `/repos/${owner}/${repo}/issues/${prNumber}/reactions`;
89-
commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${prNumber}/comments`;
90-
break;
91-
}
92-
93-
case "pull_request_review_comment": {
94-
const reviewCommentId = payload?.comment?.id;
95-
const prNumberForReviewComment = payload?.pull_request?.number;
96-
if (!reviewCommentId) {
97-
core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`);
98-
return;
99-
}
100-
if (!prNumberForReviewComment) {
101-
core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`);
102-
return;
103-
}
104-
reactionEndpoint = `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`;
105-
// Create new comment on the PR itself (using issues endpoint since PRs are issues)
106-
commentUpdateEndpoint = `/repos/${owner}/${repo}/issues/${prNumberForReviewComment}/comments`;
107-
break;
108-
}
109-
110-
case "discussion": {
111-
const discussionNumber = payload?.discussion?.number;
112-
if (!discussionNumber) {
113-
core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`);
114-
return;
115-
}
116-
// Discussions use GraphQL API - get the node ID
117-
const discussion = await getDiscussionId(owner, repo, discussionNumber);
118-
reactionEndpoint = discussion.id; // Store node ID for GraphQL
119-
commentUpdateEndpoint = `discussion:${discussionNumber}`; // Special format to indicate discussion
120-
break;
121-
}
122-
123-
case "discussion_comment": {
124-
const discussionCommentNumber = payload?.discussion?.number;
125-
const discussionCommentId = payload?.comment?.id;
126-
if (!discussionCommentNumber || !discussionCommentId) {
127-
core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`);
128-
return;
129-
}
130-
const commentNodeId = payload?.comment?.node_id;
131-
if (!commentNodeId) {
132-
core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`);
133-
return;
134-
}
135-
reactionEndpoint = commentNodeId; // Store node ID for GraphQL
136-
commentUpdateEndpoint = `discussion_comment:${discussionCommentNumber}:${discussionCommentId}`; // Special format
137-
break;
138-
}
139-
140-
default:
141-
core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`);
142-
return;
143-
}
164+
const { reactionEndpoint, commentUpdateEndpoint } = endpoints;
144165

145166
core.info(`Reaction API endpoint: ${reactionEndpoint}`);
146167

@@ -151,10 +172,8 @@ async function main() {
151172
await addReaction(reactionEndpoint, reaction);
152173
}
153174

154-
if (commentUpdateEndpoint) {
155-
core.info(`Comment endpoint: ${commentUpdateEndpoint}`);
156-
await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName, invocationContext);
157-
}
175+
core.info(`Comment endpoint: ${commentUpdateEndpoint}`);
176+
await addCommentWithWorkflowLink(commentUpdateEndpoint, runUrl, eventName, invocationContext);
158177
} catch (error) {
159178
if (isLockedError(error)) {
160179
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
319338
}
320339
}
321340

322-
module.exports = { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction };
341+
module.exports = { main, addCommentWithWorkflowLink, resolveEventEndpoints, VALID_REACTIONS, addReaction, addDiscussionReaction };

actions/setup/js/add_reaction_and_edit_comment.test.cjs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ global.context = mockContext;
3838

3939
// Helper to import the module fresh (bust module cache)
4040
async function loadModule() {
41-
const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now());
42-
return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction };
41+
const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now());
42+
return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS };
4343
}
4444

4545
describe("add_reaction_and_edit_comment.cjs", () => {
@@ -609,4 +609,88 @@ describe("add_reaction_and_edit_comment.cjs", () => {
609609
expect(mockCore.setOutput).toHaveBeenCalledWith("reaction-id", "");
610610
});
611611
});
612+
613+
describe("VALID_REACTIONS", () => {
614+
it("should export the list of valid reaction types", async () => {
615+
const { VALID_REACTIONS } = await loadModule();
616+
expect(VALID_REACTIONS).toEqual(["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]);
617+
});
618+
});
619+
620+
describe("resolveEventEndpoints()", () => {
621+
it("should resolve endpoints for issues event", async () => {
622+
const { resolveEventEndpoints } = await loadModule();
623+
const payload = { issue: { number: 42 } };
624+
const result = await resolveEventEndpoints("issues", "owner", "repo", payload);
625+
expect(result).toEqual({
626+
reactionEndpoint: "/repos/owner/repo/issues/42/reactions",
627+
commentUpdateEndpoint: "/repos/owner/repo/issues/42/comments",
628+
});
629+
});
630+
631+
it("should return null and call setFailed when issue number is missing", async () => {
632+
const { resolveEventEndpoints } = await loadModule();
633+
const result = await resolveEventEndpoints("issues", "owner", "repo", {});
634+
expect(result).toBeNull();
635+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_NOT_FOUND));
636+
});
637+
638+
it("should resolve endpoints for pull_request event", async () => {
639+
const { resolveEventEndpoints } = await loadModule();
640+
const payload = { pull_request: { number: 7 } };
641+
const result = await resolveEventEndpoints("pull_request", "owner", "repo", payload);
642+
expect(result).toEqual({
643+
reactionEndpoint: "/repos/owner/repo/issues/7/reactions",
644+
commentUpdateEndpoint: "/repos/owner/repo/issues/7/comments",
645+
});
646+
});
647+
648+
it("should resolve endpoints for issue_comment event", async () => {
649+
const { resolveEventEndpoints } = await loadModule();
650+
const payload = { comment: { id: 55 }, issue: { number: 10 } };
651+
const result = await resolveEventEndpoints("issue_comment", "owner", "repo", payload);
652+
expect(result).toEqual({
653+
reactionEndpoint: "/repos/owner/repo/issues/comments/55/reactions",
654+
commentUpdateEndpoint: "/repos/owner/repo/issues/10/comments",
655+
});
656+
});
657+
658+
it("should resolve endpoints for pull_request_review_comment event", async () => {
659+
const { resolveEventEndpoints } = await loadModule();
660+
const payload = { comment: { id: 99 }, pull_request: { number: 3 } };
661+
const result = await resolveEventEndpoints("pull_request_review_comment", "owner", "repo", payload);
662+
expect(result).toEqual({
663+
reactionEndpoint: "/repos/owner/repo/pulls/comments/99/reactions",
664+
commentUpdateEndpoint: "/repos/owner/repo/issues/3/comments",
665+
});
666+
});
667+
668+
it("should resolve endpoints for discussion event using GraphQL node ID", async () => {
669+
mockGithub.graphql.mockResolvedValueOnce({ repository: { discussion: { id: "D_node123", url: "https://github.com/testowner/testrepo/discussions/5" } } });
670+
const { resolveEventEndpoints } = await loadModule();
671+
const payload = { discussion: { number: 5 } };
672+
const result = await resolveEventEndpoints("discussion", "owner", "repo", payload);
673+
expect(result).toEqual({
674+
reactionEndpoint: "D_node123",
675+
commentUpdateEndpoint: "discussion:5",
676+
});
677+
});
678+
679+
it("should resolve endpoints for discussion_comment event", async () => {
680+
const { resolveEventEndpoints } = await loadModule();
681+
const payload = { discussion: { number: 5 }, comment: { id: 88, node_id: "DC_node88" } };
682+
const result = await resolveEventEndpoints("discussion_comment", "owner", "repo", payload);
683+
expect(result).toEqual({
684+
reactionEndpoint: "DC_node88",
685+
commentUpdateEndpoint: "discussion_comment:5:88",
686+
});
687+
});
688+
689+
it("should return null and call setFailed for unknown event type", async () => {
690+
const { resolveEventEndpoints } = await loadModule();
691+
const result = await resolveEventEndpoints("push", "owner", "repo", {});
692+
expect(result).toBeNull();
693+
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_VALIDATION));
694+
});
695+
});
612696
});

0 commit comments

Comments
 (0)