diff --git a/actions/setup/js/messages.cjs b/actions/setup/js/messages.cjs index e814536924..b8c884b42e 100644 --- a/actions/setup/js/messages.cjs +++ b/actions/setup/js/messages.cjs @@ -14,17 +14,10 @@ * - ./messages_run_status.cjs - Run status messages (getRunStartedMessage, getRunSuccessMessage, getRunFailureMessage) * - ./messages_close_discussion.cjs - Close discussion messages (getCloseOlderDiscussionMessage) * - * Supported placeholders: - * - {workflow_name} - Name of the workflow - * - {run_url} - URL to the workflow run - * - {workflow_source} - Source specification (owner/repo/path@ref) - * - {workflow_source_url} - GitHub URL for the workflow source - * - {triggering_number} - Issue/PR/Discussion number that triggered this workflow - * - {operation} - Operation name (for staged mode titles/descriptions) - * - {event_type} - Event type description (for run-started messages) - * - {status} - Workflow status text (for run-failure messages) - * + * This module supports placeholder-based templates for messages. * Both camelCase and snake_case placeholder formats are supported. + * For the authoritative and up-to-date list of supported placeholders, + * see the documentation in ./messages_core.cjs. */ // Re-export core utilities diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs index e38d21b39b..546431a168 100644 --- a/actions/setup/js/messages.test.cjs +++ b/actions/setup/js/messages.test.cjs @@ -165,7 +165,7 @@ describe("messages.cjs", () => { runUrl: "https://github.com/test/repo/actions/runs/123", }); - expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)"); + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/agentic_workflow)"); }); it("should append triggering number when provided", async () => { @@ -177,7 +177,7 @@ describe("messages.cjs", () => { triggeringNumber: 42, }); - expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for issue #42"); + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/agentic_workflow) for issue #42"); }); it("should use custom footer template", async () => { @@ -252,7 +252,7 @@ describe("messages.cjs", () => { historyUrl: "https://github.com/search?q=repo:test/repo+is:issue&type=issues", }); - expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)"); + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/agentic_workflow) · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)"); }); it("should include both triggering number and history link when both are provided", async () => { @@ -265,7 +265,7 @@ describe("messages.cjs", () => { historyUrl: "https://github.com/search?q=repo:test/repo+is:issue&type=issues", }); - expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for issue #42 · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)"); + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/agentic_workflow) for issue #42 · [◷](https://github.com/search?q=repo:test/repo+is:issue&type=issues)"); }); it("should not append history link when historyUrl is not provided", async () => { @@ -312,6 +312,34 @@ describe("messages.cjs", () => { expect(result).toBe("> 🤖 *Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)*"); expect(result).not.toContain("{history_link}"); }); + + it("should expose {agentic_workflow_url} placeholder in custom footer templates", async () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + footer: "> Generated by [{workflow_name}]({agentic_workflow_url})", + }); + + const { getFooterMessage } = await import("./messages.cjs"); + + const result = getFooterMessage({ + workflowName: "Test Workflow", + runUrl: "https://github.com/test/repo/actions/runs/123", + }); + + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/agentic_workflow)"); + }); + + it("should use explicit agenticWorkflowUrl from context instead of computing from runUrl", async () => { + const { getFooterMessage } = await import("./messages.cjs"); + + const result = getFooterMessage({ + workflowName: "Test Workflow", + runUrl: "https://github.com/test/repo/actions/runs/123", + agenticWorkflowUrl: "https://github.com/test/repo/actions/runs/123/custom_path", + }); + + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123/custom_path)"); + expect(result).not.toContain("/agentic_workflow"); + }); }); describe("getFooterInstallMessage", () => { diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs index ecd4fa40e5..f31e7d8641 100644 --- a/actions/setup/js/messages_core.cjs +++ b/actions/setup/js/messages_core.cjs @@ -10,6 +10,7 @@ * Supported placeholders: * - {workflow_name} - Name of the workflow * - {run_url} - URL to the workflow run + * - {agentic_workflow_url} - Direct URL to the agentic workflow page ({run_url}/agentic_workflow) * - {workflow_source} - Source specification (owner/repo/path@ref) * - {workflow_source_url} - GitHub URL for the workflow source * - {triggering_number} - Issue/PR/Discussion number that triggered this workflow diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index 2e41960db1..afc6e3e222 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -17,6 +17,7 @@ const { getDifcFilteredEvents, generateDifcFilteredSection } = require("./gatewa * @typedef {Object} FooterContext * @property {string} workflowName - Name of the workflow * @property {string} runUrl - URL of the workflow run + * @property {string} [agenticWorkflowUrl] - Direct URL to the agentic workflow page ({run_url}/agentic_workflow) * @property {string} [workflowSource] - Source of the workflow (owner/repo/path@ref) * @property {string} [workflowSourceUrl] - GitHub URL for the workflow source * @property {number|string} [triggeringNumber] - Issue, PR, or discussion number that triggered this workflow @@ -35,8 +36,11 @@ function getFooterMessage(ctx) { // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable) const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : ""; - // Create context with both camelCase and snake_case keys, including computed history_link - const templateContext = toSnakeCase({ ...ctx, historyLink }); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + + // Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url + const templateContext = toSnakeCase({ ...ctx, historyLink, agenticWorkflowUrl }); // Use custom footer template if configured (no automatic suffix appended) if (messages?.footer) { @@ -44,7 +48,7 @@ function getFooterMessage(ctx) { } // Default footer template - includes triggering reference if available - let defaultFooter = "> Generated by [{workflow_name}]({run_url})"; + let defaultFooter = "> Generated by [{workflow_name}]({agentic_workflow_url})"; if (ctx.triggeringNumber) { defaultFooter += " for issue #{triggering_number}"; } @@ -67,8 +71,11 @@ function getFooterInstallMessage(ctx) { const messages = getMessages(); - // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + + // Create context with both camelCase and snake_case keys, including computed agentic_workflow_url + const templateContext = toSnakeCase({ ...ctx, agenticWorkflowUrl }); // Default installation template const defaultInstall = "> To install this [agentic workflow]({workflow_source_url}), run\n> ```\n> gh aw add {workflow_source}\n> ```"; @@ -81,6 +88,7 @@ function getFooterInstallMessage(ctx) { * @typedef {Object} WorkflowRecompileContext * @property {string} workflowName - Name of the workflow * @property {string} runUrl - URL of the workflow run + * @property {string} [agenticWorkflowUrl] - Direct URL to the agentic workflow page ({run_url}/agentic_workflow) * @property {string} repository - Repository name (owner/repo) */ @@ -92,11 +100,14 @@ function getFooterInstallMessage(ctx) { function getFooterWorkflowRecompileMessage(ctx) { const messages = getMessages(); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); + const templateContext = toSnakeCase({ ...ctx, agenticWorkflowUrl }); // Default footer template - const defaultFooter = "> Generated by [{workflow_name}]({run_url})"; + const defaultFooter = "> Generated by [{workflow_name}]({agentic_workflow_url})"; // Use custom workflow recompile footer if configured, otherwise use default footer return messages?.footerWorkflowRecompile ? renderTemplate(messages.footerWorkflowRecompile, templateContext) : renderTemplate(defaultFooter, templateContext); @@ -110,11 +121,14 @@ function getFooterWorkflowRecompileMessage(ctx) { function getFooterWorkflowRecompileCommentMessage(ctx) { const messages = getMessages(); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + // Create context with both camelCase and snake_case keys - const templateContext = toSnakeCase(ctx); + const templateContext = toSnakeCase({ ...ctx, agenticWorkflowUrl }); // Default footer template - const defaultFooter = "> Updated by [{workflow_name}]({run_url})"; + const defaultFooter = "> Updated by [{workflow_name}]({agentic_workflow_url})"; // Use custom workflow recompile comment footer if configured, otherwise use default footer return messages?.footerWorkflowRecompileComment ? renderTemplate(messages.footerWorkflowRecompileComment, templateContext) : renderTemplate(defaultFooter, templateContext); @@ -124,6 +138,7 @@ function getFooterWorkflowRecompileCommentMessage(ctx) { * @typedef {Object} AgentFailureContext * @property {string} workflowName - Name of the workflow * @property {string} runUrl - URL of the workflow run + * @property {string} [agenticWorkflowUrl] - Direct URL to the agentic workflow page ({run_url}/agentic_workflow) * @property {string} [workflowSource] - Source of the workflow (owner/repo/path@ref) * @property {string} [workflowSourceUrl] - GitHub URL for the workflow source * @property {string} [historyUrl] - GitHub search URL for issues created by this workflow (for the history link) @@ -140,8 +155,11 @@ function getFooterAgentFailureIssueMessage(ctx) { // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable) const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : ""; - // Create context with both camelCase and snake_case keys, including computed history_link - const templateContext = toSnakeCase({ ...ctx, historyLink }); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + + // Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url + const templateContext = toSnakeCase({ ...ctx, historyLink, agenticWorkflowUrl }); // Use custom agent failure issue footer if configured, otherwise use default footer if (messages?.agentFailureIssue) { @@ -149,7 +167,7 @@ function getFooterAgentFailureIssueMessage(ctx) { } // Default footer template with link to workflow run - let defaultFooter = "> Generated from [{workflow_name}]({run_url})"; + let defaultFooter = "> Generated from [{workflow_name}]({agentic_workflow_url})"; // Append history link when available if (ctx.historyUrl) { defaultFooter += " · [◷]({history_url})"; @@ -168,8 +186,11 @@ function getFooterAgentFailureCommentMessage(ctx) { // Pre-compute history_link as a ready-to-use markdown suffix (empty string when unavailable) const historyLink = ctx.historyUrl ? ` · [◷](${ctx.historyUrl})` : ""; - // Create context with both camelCase and snake_case keys, including computed history_link - const templateContext = toSnakeCase({ ...ctx, historyLink }); + // Pre-compute agentic_workflow_url as the direct link to the agentic workflow page + const agenticWorkflowUrl = ctx.agenticWorkflowUrl || (ctx.runUrl ? `${ctx.runUrl}/agentic_workflow` : ""); + + // Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url + const templateContext = toSnakeCase({ ...ctx, historyLink, agenticWorkflowUrl }); // Use custom agent failure comment footer if configured, otherwise use default footer if (messages?.agentFailureComment) { @@ -177,7 +198,7 @@ function getFooterAgentFailureCommentMessage(ctx) { } // Default footer template with link to workflow run - let defaultFooter = "> Generated from [{workflow_name}]({run_url})"; + let defaultFooter = "> Generated from [{workflow_name}]({agentic_workflow_url})"; // Append history link when available if (ctx.historyUrl) { defaultFooter += " · [◷]({history_url})"; diff --git a/pkg/workflow/cache_memory_threat_detection_test.go b/pkg/workflow/cache_memory_threat_detection_test.go index 4f8ae1820c..dab50ef246 100644 --- a/pkg/workflow/cache_memory_threat_detection_test.go +++ b/pkg/workflow/cache_memory_threat_detection_test.go @@ -53,7 +53,7 @@ Test workflow with cache-memory and threat detection enabled.`, // Should have update_cache_memory job (depends on detection job) "update_cache_memory:", "- detection", - "if: always() && needs.detection.result == 'success'", + "if: always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped')", "- name: Download cache-memory artifact (default)", "- name: Save cache-memory to cache (default)", "uses: actions/cache/save@",