diff --git a/actions/setup/js/add_reaction.cjs b/actions/setup/js/add_reaction.cjs index ad857c6324e..0c368d9076c 100644 --- a/actions/setup/js/add_reaction.cjs +++ b/actions/setup/js/add_reaction.cjs @@ -3,7 +3,7 @@ const { getErrorMessage, isLockedError } = require("./error_helpers.cjs"); const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); -const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); +const { resolveReactionSetup } = require("./reaction_setup_helpers.cjs"); /** @type {Record} Maps REST reaction names to GraphQL ReactionContent enum values */ const REACTION_MAP = { @@ -24,20 +24,15 @@ const REACTION_MAP = { * Use add_reaction_and_edit_comment.cjs in the activation job to create the comment with workflow link. */ async function main() { - // Read inputs from environment variables - const reaction = process.env.GH_AW_REACTION || "eyes"; - - core.info(`Adding reaction: ${reaction}`); - - // Validate reaction type - const validReactions = Object.keys(REACTION_MAP); - if (!validReactions.includes(reaction)) { - core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); + const setup = resolveReactionSetup(context); + if (!setup) { return; } + const { reaction, invocationContext } = setup; + + core.info(`Adding reaction: ${reaction}`); // Determine the API endpoint based on the event type - const invocationContext = resolveInvocationContext(context); const eventName = invocationContext.eventName; const { owner, repo } = invocationContext.eventRepo; const payload = invocationContext.eventPayload; diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index fa00fedee60..431607be821 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -8,7 +8,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 { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); +const { VALID_REACTIONS, resolveReactionSetup } = require("./reaction_setup_helpers.cjs"); const { addReaction, addDiscussionReaction, getDiscussionNodeId } = require("./add_reaction.cjs"); /** @@ -24,9 +24,6 @@ const EVENT_TYPE_DESCRIPTIONS = { discussion_comment: "discussion comment", }; -/** Valid GitHub reaction types */ -const VALID_REACTIONS = Object.freeze(["+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. @@ -138,10 +135,13 @@ async function resolveEventEndpoints(eventName, owner, repo, payload) { } async function main() { - const reaction = process.env.GH_AW_REACTION || "eyes"; + const setup = resolveReactionSetup(context); + if (!setup) { + return; + } + const { reaction, invocationContext } = setup; const commandsJSON = process.env.GH_AW_COMMANDS; const command = commandsJSON ? (JSON.parse(commandsJSON)[0] ?? null) : null; // Only present for command workflows - const invocationContext = resolveInvocationContext(context); const runUrl = buildWorkflowRunUrl(context, invocationContext.workflowRepo); core.info(`Reaction type: ${reaction}`); @@ -149,11 +149,6 @@ async function main() { core.info(`Run ID: ${context.runId}`); core.info(`Run URL: ${runUrl}`); - if (!VALID_REACTIONS.includes(reaction)) { - core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${VALID_REACTIONS.join(", ")}`); - return; - } - const eventName = invocationContext.eventName; const { owner, repo } = invocationContext.eventRepo; const payload = invocationContext.eventPayload; diff --git a/actions/setup/js/reaction_setup_helpers.cjs b/actions/setup/js/reaction_setup_helpers.cjs new file mode 100644 index 00000000000..a3a00cedb6c --- /dev/null +++ b/actions/setup/js/reaction_setup_helpers.cjs @@ -0,0 +1,25 @@ +// @ts-check +/// + +const { ERR_VALIDATION } = require("./error_codes.cjs"); +const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); + +/** Valid GitHub reaction types */ +const VALID_REACTIONS = Object.freeze(["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]); + +/** + * Resolve and validate reaction input plus invocation context. + * @param {any} rawContext + * @returns {{ reaction: string, invocationContext: ReturnType } | null } + */ +function resolveReactionSetup(rawContext) { + const reaction = process.env.GH_AW_REACTION || "eyes"; + if (!VALID_REACTIONS.includes(reaction)) { + core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${VALID_REACTIONS.join(", ")}`); + return null; + } + const invocationContext = resolveInvocationContext(rawContext); + return { reaction, invocationContext }; +} + +module.exports = { VALID_REACTIONS, resolveReactionSetup }; diff --git a/actions/setup/js/reaction_setup_helpers.test.cjs b/actions/setup/js/reaction_setup_helpers.test.cjs new file mode 100644 index 00000000000..9d64aae15c1 --- /dev/null +++ b/actions/setup/js/reaction_setup_helpers.test.cjs @@ -0,0 +1,67 @@ +// @ts-check +import { describe, it, expect, beforeEach, vi } from "vitest"; + +const mockCore = { + setFailed: vi.fn(), +}; + +global.core = mockCore; + +describe("reaction_setup_helpers", () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.resetModules(); + delete process.env.GH_AW_REACTION; + }); + + async function importHelpers() { + return import("./reaction_setup_helpers.cjs?" + Date.now()); + } + + it("uses eyes as the default reaction", async () => { + const { resolveReactionSetup } = await importHelpers(); + + const result = resolveReactionSetup({ + eventName: "issues", + repo: { owner: "testowner", repo: "testrepo" }, + payload: { issue: { number: 123 } }, + }); + + expect(result?.reaction).toBe("eyes"); + expect(result?.invocationContext.eventName).toBe("issues"); + }); + + it("uses GH_AW_REACTION when provided", async () => { + process.env.GH_AW_REACTION = "rocket"; + const { resolveReactionSetup } = await importHelpers(); + + const result = resolveReactionSetup({ + eventName: "issues", + repo: { owner: "testowner", repo: "testrepo" }, + payload: { issue: { number: 123 } }, + }); + + expect(result?.reaction).toBe("rocket"); + }); + + it("fails and returns null for invalid reaction", async () => { + process.env.GH_AW_REACTION = "invalid"; + const { resolveReactionSetup } = await importHelpers(); + + const result = resolveReactionSetup({ + eventName: "issues", + repo: { owner: "testowner", repo: "testrepo" }, + payload: { issue: { number: 123 } }, + }); + + expect(result).toBeNull(); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Invalid reaction type: invalid")); + }); + + it("VALID_REACTIONS stays in sync with REACTION_MAP keys", async () => { + const { VALID_REACTIONS } = await importHelpers(); + const { REACTION_MAP } = await import("./add_reaction.cjs?" + Date.now()); + + expect([...VALID_REACTIONS].sort()).toEqual(Object.keys(REACTION_MAP).sort()); + }); +});