diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs index 88c95236409..e1d7e20526e 100644 --- a/actions/setup/js/messages_run_status.cjs +++ b/actions/setup/js/messages_run_status.cjs @@ -1,5 +1,4 @@ // @ts-check -/// /** * Run Status Message Module @@ -10,6 +9,19 @@ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); +/** + * Renders a message using a custom template from config or a default template. + * @param {string} messageKey - Key in the messages config (e.g., "runStarted") + * @param {string} defaultTemplate - Default template string with {placeholder} syntax + * @param {Object} ctx - Context object for template substitution + * @returns {string} Rendered message + */ +function renderConfiguredMessage(messageKey, defaultTemplate, ctx) { + const messages = getMessages(); + const template = messages?.[messageKey] ?? defaultTemplate; + return renderTemplate(template, toSnakeCase(ctx)); +} + /** * @typedef {Object} RunStartedContext * @property {string} workflowName - Name of the workflow @@ -23,16 +35,7 @@ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cj * @returns {string} Run-started message */ function getRunStartedMessage(ctx) { - const messages = getMessages(); - - // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); - - // Default run-started template - const defaultMessage = "🚀 [{workflow_name}]({run_url}) has started processing this {event_type}"; - - // Use custom message if configured - return messages?.runStarted ? renderTemplate(messages.runStarted, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("runStarted", "🚀 [{workflow_name}]({run_url}) has started processing this {event_type}", ctx); } /** @@ -47,16 +50,7 @@ function getRunStartedMessage(ctx) { * @returns {string} Run-success message */ function getRunSuccessMessage(ctx) { - const messages = getMessages(); - - // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); - - // Default run-success template - const defaultMessage = "✅ [{workflow_name}]({run_url}) completed successfully!"; - - // Use custom message if configured - return messages?.runSuccess ? renderTemplate(messages.runSuccess, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("runSuccess", "✅ [{workflow_name}]({run_url}) completed successfully!", ctx); } /** @@ -72,16 +66,7 @@ function getRunSuccessMessage(ctx) { * @returns {string} Run-failure message */ function getRunFailureMessage(ctx) { - const messages = getMessages(); - - // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); - - // Default run-failure template - const defaultMessage = "❌ [{workflow_name}]({run_url}) {status}. Please review the logs for details."; - - // Use custom message if configured - return messages?.runFailure ? renderTemplate(messages.runFailure, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("runFailure", "❌ [{workflow_name}]({run_url}) {status}. Please review the logs for details.", ctx); } /** @@ -96,16 +81,7 @@ function getRunFailureMessage(ctx) { * @returns {string} Detection-failure message */ function getDetectionFailureMessage(ctx) { - const messages = getMessages(); - - // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); - - // Default detection-failure template - const defaultMessage = "⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details."; - - // Use custom message if configured - return messages?.detectionFailure ? renderTemplate(messages.detectionFailure, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("detectionFailure", "⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", ctx); } /** @@ -120,10 +96,7 @@ function getDetectionFailureMessage(ctx) { * @returns {string} Pull-request-created message */ function getPullRequestCreatedMessage(ctx) { - const messages = getMessages(); - const templateContext = toSnakeCase(ctx); - const defaultMessage = "Pull request created: [#{item_number}]({item_url})"; - return messages?.pullRequestCreated ? renderTemplate(messages.pullRequestCreated, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("pullRequestCreated", "Pull request created: [#{item_number}]({item_url})", ctx); } /** @@ -138,10 +111,7 @@ function getPullRequestCreatedMessage(ctx) { * @returns {string} Issue-created message */ function getIssueCreatedMessage(ctx) { - const messages = getMessages(); - const templateContext = toSnakeCase(ctx); - const defaultMessage = "Issue created: [#{item_number}]({item_url})"; - return messages?.issueCreated ? renderTemplate(messages.issueCreated, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("issueCreated", "Issue created: [#{item_number}]({item_url})", ctx); } /** @@ -157,10 +127,7 @@ function getIssueCreatedMessage(ctx) { * @returns {string} Commit-pushed message */ function getCommitPushedMessage(ctx) { - const messages = getMessages(); - const templateContext = toSnakeCase(ctx); - const defaultMessage = "Commit pushed: [`{short_sha}`]({commit_url})"; - return messages?.commitPushed ? renderTemplate(messages.commitPushed, templateContext) : renderTemplate(defaultMessage, templateContext); + return renderConfiguredMessage("commitPushed", "Commit pushed: [`{short_sha}`]({commit_url})", ctx); } module.exports = { diff --git a/actions/setup/js/messages_run_status.test.cjs b/actions/setup/js/messages_run_status.test.cjs new file mode 100644 index 00000000000..cf28a6d9537 --- /dev/null +++ b/actions/setup/js/messages_run_status.test.cjs @@ -0,0 +1,156 @@ +// @ts-check +import { describe, it, expect, beforeEach, vi } from "vitest"; + +// messages_core.cjs calls core.warning on parse failures - provide a stub +const mockCore = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), +}; +global.core = mockCore; + +const { getRunStartedMessage, getRunSuccessMessage, getRunFailureMessage, getDetectionFailureMessage, getPullRequestCreatedMessage, getIssueCreatedMessage, getCommitPushedMessage } = require("./messages_run_status.cjs"); + +const WORKFLOW = "My Workflow"; +const RUN_URL = "https://github.com/owner/repo/actions/runs/99"; + +describe("messages_run_status", () => { + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES; + }); + + describe("getRunStartedMessage", () => { + it("returns default template with all placeholders substituted", () => { + const msg = getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "issue" }); + expect(msg).toBe(`🚀 [${WORKFLOW}](${RUN_URL}) has started processing this issue`); + }); + + it("supports different event types", () => { + expect(getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "pull request" })).toContain("pull request"); + expect(getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "discussion" })).toContain("discussion"); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runStarted: "Custom: {workflow_name} started" }); + const msg = getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "issue" }); + expect(msg).toBe(`Custom: ${WORKFLOW} started`); + }); + + it("substitutes camelCase keys as well as snake_case", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runStarted: "{workflowName} at {runUrl}" }); + const msg = getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "issue" }); + expect(msg).toBe(`${WORKFLOW} at ${RUN_URL}`); + }); + }); + + describe("getRunSuccessMessage", () => { + it("returns default template with placeholders substituted", () => { + const msg = getRunSuccessMessage({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(msg).toBe(`✅ [${WORKFLOW}](${RUN_URL}) completed successfully!`); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runSuccess: "Done: {workflow_name}" }); + const msg = getRunSuccessMessage({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(msg).toBe(`Done: ${WORKFLOW}`); + }); + + it("ignores unrelated config keys and uses default", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runStarted: "overridden" }); + const msg = getRunSuccessMessage({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(msg).toContain("completed successfully"); + }); + }); + + describe("getRunFailureMessage", () => { + it("returns default template with status substituted", () => { + const msg = getRunFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, status: "failed" }); + expect(msg).toBe(`❌ [${WORKFLOW}](${RUN_URL}) failed. Please review the logs for details.`); + }); + + it("handles different status values", () => { + expect(getRunFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, status: "was cancelled" })).toContain("was cancelled"); + expect(getRunFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, status: "timed out" })).toContain("timed out"); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ runFailure: "FAILED: {workflow_name} - {status}" }); + const msg = getRunFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, status: "failed" }); + expect(msg).toBe(`FAILED: ${WORKFLOW} - failed`); + }); + }); + + describe("getDetectionFailureMessage", () => { + it("returns default template with placeholders substituted", () => { + const msg = getDetectionFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(msg).toBe(`⚠️ Security scanning failed for [${WORKFLOW}](${RUN_URL}). Review the logs for details.`); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ detectionFailure: "Security alert for {workflow_name}" }); + const msg = getDetectionFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(msg).toBe(`Security alert for ${WORKFLOW}`); + }); + }); + + describe("getPullRequestCreatedMessage", () => { + it("returns default template with item_number and item_url substituted", () => { + const msg = getPullRequestCreatedMessage({ itemNumber: 42, itemUrl: "https://github.com/owner/repo/pull/42" }); + expect(msg).toBe("Pull request created: [#42](https://github.com/owner/repo/pull/42)"); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ pullRequestCreated: "PR #{item_number} ready" }); + const msg = getPullRequestCreatedMessage({ itemNumber: 7, itemUrl: "https://github.com/owner/repo/pull/7" }); + expect(msg).toBe("PR #7 ready"); + }); + }); + + describe("getIssueCreatedMessage", () => { + it("returns default template with item_number and item_url substituted", () => { + const msg = getIssueCreatedMessage({ itemNumber: 15, itemUrl: "https://github.com/owner/repo/issues/15" }); + expect(msg).toBe("Issue created: [#15](https://github.com/owner/repo/issues/15)"); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ issueCreated: "New issue #{item_number}" }); + const msg = getIssueCreatedMessage({ itemNumber: 3, itemUrl: "https://github.com/owner/repo/issues/3" }); + expect(msg).toBe("New issue #3"); + }); + }); + + describe("getCommitPushedMessage", () => { + const SHA = "abc1234def5678901234567890123456789012ab"; + const SHORT = "abc1234"; + const COMMIT_URL = `https://github.com/owner/repo/commit/${SHA}`; + + it("returns default template with short_sha and commit_url substituted", () => { + const msg = getCommitPushedMessage({ commitSha: SHA, shortSha: SHORT, commitUrl: COMMIT_URL }); + expect(msg).toBe(`Commit pushed: [\`${SHORT}\`](${COMMIT_URL})`); + }); + + it("uses custom template from config", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ commitPushed: "Pushed {short_sha} to repo" }); + const msg = getCommitPushedMessage({ commitSha: SHA, shortSha: SHORT, commitUrl: COMMIT_URL }); + expect(msg).toBe(`Pushed ${SHORT} to repo`); + }); + + it("supports full SHA in custom template", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ commitPushed: "{commit_sha}" }); + const msg = getCommitPushedMessage({ commitSha: SHA, shortSha: SHORT, commitUrl: COMMIT_URL }); + expect(msg).toBe(SHA); + }); + }); + + describe("fallback when config is missing keys", () => { + it("uses default template when only unrelated config keys are set", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ footer: "custom footer" }); + expect(getRunStartedMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, eventType: "issue" })).toContain("has started processing"); + expect(getRunSuccessMessage({ workflowName: WORKFLOW, runUrl: RUN_URL })).toContain("completed successfully"); + expect(getRunFailureMessage({ workflowName: WORKFLOW, runUrl: RUN_URL, status: "failed" })).toContain("failed"); + }); + }); +});