From 6c5a8d935e769dffeff422f1f90623b8bd35994b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 23:57:31 +0000 Subject: [PATCH 1/2] feat: add body-header message type to safe outputs Adds a new `body-header` message type to the safe-outputs messages system. When configured, it prepends custom text to every message body generated by safe outputs (issues, comments, pull requests, discussions). The header is placed after any threat-detection caution alert and before the agent-generated content. Changes: - Add `body-header` field to JSON schema with placeholders {workflow_name} and {run_url} - Add `BodyHeader string` field to `SafeOutputMessagesConfig` Go struct - Add parser for `body-header` in safe_outputs_messages_config.go - Add `bodyHeader` to `SafeOutputMessages` typedef in messages_core.cjs - Create messages_header.cjs with getBodyHeader() function - Create messages_header.test.cjs with 6 tests - Update messages.cjs barrel file with re-export - Apply header injection in create_issue.cjs, add_comment.cjs, create_discussion.cjs, and create_pull_request.cjs - Add Go unit tests for body-header parsing and serialization Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1769a58a-3232-4313-89ce-33a6b2cfc9ef Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/add_comment.cjs | 13 +++- actions/setup/js/create_discussion.cjs | 7 ++ actions/setup/js/create_issue.cjs | 7 ++ actions/setup/js/create_pull_request.cjs | 7 ++ actions/setup/js/messages.cjs | 4 ++ actions/setup/js/messages_core.cjs | 1 + actions/setup/js/messages_header.cjs | 38 ++++++++++ actions/setup/js/messages_header.test.cjs | 76 ++++++++++++++++++++ pkg/parser/schemas/main_workflow_schema.json | 5 ++ pkg/workflow/compiler_types.go | 1 + pkg/workflow/safe_outputs_messages_config.go | 1 + pkg/workflow/safe_outputs_messages_test.go | 53 ++++++++++++++ 12 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 actions/setup/js/messages_header.cjs create mode 100644 actions/setup/js/messages_header.test.cjs diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index bbb62304d4b..068d8fd9f86 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -17,6 +17,7 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { getMissingInfoSections } = require("./missing_messages_helper.cjs"); const { getMessages } = require("./messages_core.cjs"); +const { getBodyHeader } = require("./messages_header.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs"); const { resolveTopLevelDiscussionCommentId } = require("./github_api_helpers.cjs"); @@ -582,9 +583,15 @@ async function main(config = {}) { // Inject CAUTION at top of body if threat detection warning was raised const detectionCaution = getDetectionCautionAlert(workflowName, runUrl); - if (detectionCaution) { - processedBody = detectionCaution + "\n\n" + processedBody; - } + + // Inject body header if configured (placed after caution, before user content) + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + + // Build prefix: caution (if any) → body header (if any) → user content + let prefix = ""; + if (detectionCaution) prefix += detectionCaution + "\n\n"; + if (bodyHeader) prefix += bodyHeader + "\n\n"; + if (prefix) processedBody = prefix + processedBody; // Add tracker ID and footer const trackerIDComment = getTrackerID("markdown"); diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 971650ef431..60c0a909b31 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -18,6 +18,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_VALIDATION } = require("./error_codes.cjs"); const { createExpirationLine, generateFooterWithExpiration, addExpirationToFooter } = require("./ephemerals.cjs"); const { generateFooterWithMessages, getDetectionCautionAlert } = require("./messages_footer.cjs"); +const { getBodyHeader } = require("./messages_header.cjs"); const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); @@ -521,6 +522,12 @@ async function main(config = {}) { bodyLines.unshift(...detectionCaution.split("\n"), ""); } + // Inject body header after any caution alert, before user content + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + // Generate footer with expiration using helper // When footer is disabled, only add XML markers (no visible footer content) if (includeFooter) { diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 481abbdd467..e321cb4b393 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -5,6 +5,7 @@ const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { generateFooterWithMessages, getDetectionCautionAlert } = require("./messages_footer.cjs"); +const { getBodyHeader } = require("./messages_header.cjs"); const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { getTrackerID } = require("./get_tracker_id.cjs"); @@ -453,6 +454,12 @@ async function main(config = {}) { bodyLines.unshift(...detectionCaution.split("\n"), ""); } + // Inject body header after any caution alert, before user content + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + // Add tracker-id comment if present const trackerIDComment = getTrackerID("markdown"); if (trackerIDComment) { diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 286acece4ea..5fd61922295 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -18,6 +18,7 @@ const { addExpirationToFooter } = require("./ephemerals.cjs"); const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { generateFooterWithMessages, getDetectionCautionAlert } = require("./messages_footer.cjs"); +const { getBodyHeader } = require("./messages_header.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs"); @@ -1133,6 +1134,12 @@ async function main(config = {}) { bodyLines.unshift(detectionCaution, "", ""); } + // Inject body header after any caution alert, before user content + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + // Add fingerprint comment if present const trackerIDComment = getTrackerID("markdown"); if (trackerIDComment) { diff --git a/actions/setup/js/messages.cjs b/actions/setup/js/messages.cjs index 1f21f933e6a..83120ed2215 100644 --- a/actions/setup/js/messages.cjs +++ b/actions/setup/js/messages.cjs @@ -35,6 +35,9 @@ const { getRunStartedMessage, getRunSuccessMessage, getRunFailureMessage } = req // Re-export close discussion messages const { getCloseOlderDiscussionMessage } = require("./messages_close_discussion.cjs"); +// Re-export body header messages +const { getBodyHeader } = require("./messages_header.cjs"); + module.exports = { getMessages, renderTemplate, @@ -52,4 +55,5 @@ module.exports = { getRunSuccessMessage, getRunFailureMessage, getCloseOlderDiscussionMessage, + getBodyHeader, }; diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs index 646f467f6f7..5a0d7b545c5 100644 --- a/actions/setup/js/messages_core.cjs +++ b/actions/setup/js/messages_core.cjs @@ -47,6 +47,7 @@ const fs = require("fs"); * @property {string} [agentFailureIssue] - Custom footer template for agent failure tracking issues * @property {string} [agentFailureComment] - Custom footer template for comments on agent failure tracking issues * @property {string} [closeOlderDiscussion] - Custom message for closing older discussions as outdated + * @property {string} [bodyHeader] - Custom header text prepended to every message body (issues, comments, PRs, discussions). Placeholders: {workflow_name}, {run_url} * @property {boolean} [appendOnlyComments] - If true, create new comments instead of updating the activation comment * @property {string|boolean} [activationComments] - If false or "false", disable all activation/fallback comments entirely. Supports templatable boolean values (default: true) */ diff --git a/actions/setup/js/messages_header.cjs b/actions/setup/js/messages_header.cjs new file mode 100644 index 00000000000..415e0d8d4e1 --- /dev/null +++ b/actions/setup/js/messages_header.cjs @@ -0,0 +1,38 @@ +// @ts-check +/// + +/** + * Body Header Message Module + * + * This module provides the body-header generation for safe-output workflows. + * The body header is prepended to every message body generated by safe outputs + * (issues, comments, pull requests, discussions). + */ + +const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); + +/** + * @typedef {Object} BodyHeaderContext + * @property {string} workflowName - Name of the workflow + * @property {string} runUrl - URL of the workflow run + */ + +/** + * Get the body header text, using the custom template if configured. + * Returns an empty string when no body-header is configured. + * @param {BodyHeaderContext} ctx - Context for header generation + * @returns {string} The rendered header text, or empty string if not configured + */ +function getBodyHeader(ctx) { + const messages = getMessages(); + if (!messages?.bodyHeader) { + return ""; + } + + const templateContext = toSnakeCase(ctx); + return renderTemplate(messages.bodyHeader, templateContext); +} + +module.exports = { + getBodyHeader, +}; diff --git a/actions/setup/js/messages_header.test.cjs b/actions/setup/js/messages_header.test.cjs new file mode 100644 index 00000000000..f2dd5cf02fb --- /dev/null +++ b/actions/setup/js/messages_header.test.cjs @@ -0,0 +1,76 @@ +// @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 { getBodyHeader } = require("./messages_header.cjs"); + +const WORKFLOW = "My Workflow"; +const RUN_URL = "https://github.com/owner/repo/actions/runs/99"; + +describe("messages_header", () => { + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES; + }); + + describe("getBodyHeader", () => { + it("returns empty string when no body-header is configured", () => { + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(""); + }); + + it("returns rendered template when body-header is configured", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + bodyHeader: "> ⚠️ Generated by [{workflow_name}]({run_url}).", + }); + + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(`> ⚠️ Generated by [${WORKFLOW}](${RUN_URL}).`); + }); + + it("supports snake_case placeholders", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + bodyHeader: "> Generated by {workflow_name}.", + }); + + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(`> Generated by ${WORKFLOW}.`); + }); + + it("supports camelCase placeholders", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + bodyHeader: "> Run: {runUrl}", + }); + + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(`> Run: ${RUN_URL}`); + }); + + it("returns empty string when messages env is set but body-header is absent", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + footer: "> Custom footer", + }); + + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(""); + }); + + it("leaves unknown placeholders unchanged", () => { + process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ + bodyHeader: "> Header {workflow_name} {unknown_placeholder}", + }); + + const result = getBodyHeader({ workflowName: WORKFLOW, runUrl: RUN_URL }); + expect(result).toBe(`> Header ${WORKFLOW} {unknown_placeholder}`); + }); + }); +}); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 3aaa67b6b80..abb546a731f 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -8621,6 +8621,11 @@ "description": "Custom message template for commit push link appended to the activation comment. Available placeholders: {commit_sha}, {short_sha}, {commit_url}. Default: 'Commit pushed: [`{short_sha}`]({commit_url})'", "examples": ["Commit pushed: [`{short_sha}`]({commit_url})", "[`{short_sha}`]({commit_url}) pushed"] }, + "body-header": { + "type": "string", + "description": "Custom header text prepended to every message body generated by safe outputs (issues, comments, pull requests, discussions). Applied after any threat-detection caution alert and before the agent-generated content. Available placeholders: {workflow_name}, {run_url}.", + "examples": ["> ⚠️ This content was generated by [{workflow_name}]({run_url}).", "> 🤖 AI-generated output — please review before acting."] + }, "append-only-comments": { "type": "boolean", "description": "When enabled, workflow completion notifier creates a new comment instead of editing the activation comment. Creates an append-only timeline of workflow runs. Default: false", diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 8a41fce3435..636c3e5fe41 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -652,6 +652,7 @@ type SafeOutputMessagesConfig struct { CommitPushed string `yaml:"commit-pushed,omitempty" json:"commitPushed,omitempty"` // Custom message template for commit push link. Placeholders: {commit_sha}, {short_sha}, {commit_url} AgentFailureIssue string `yaml:"agent-failure-issue,omitempty" json:"agentFailureIssue,omitempty"` // Custom footer template for agent failure tracking issues AgentFailureComment string `yaml:"agent-failure-comment,omitempty" json:"agentFailureComment,omitempty"` // Custom footer template for comments on agent failure tracking issues + BodyHeader string `yaml:"body-header,omitempty" json:"bodyHeader,omitempty"` // Custom header text prepended to every message body (issues, comments, PRs, discussions). Placeholders: {workflow_name}, {run_url} } // MentionsConfig holds configuration for @mention filtering in safe outputs diff --git a/pkg/workflow/safe_outputs_messages_config.go b/pkg/workflow/safe_outputs_messages_config.go index 9c772afe036..c48abe36cab 100644 --- a/pkg/workflow/safe_outputs_messages_config.go +++ b/pkg/workflow/safe_outputs_messages_config.go @@ -40,6 +40,7 @@ func parseMessagesConfig(messagesMap map[string]any) *SafeOutputMessagesConfig { config.CommitPushed = extractStringFromMap(messagesMap, "commit-pushed", nil) config.AgentFailureIssue = extractStringFromMap(messagesMap, "agent-failure-issue", nil) config.AgentFailureComment = extractStringFromMap(messagesMap, "agent-failure-comment", nil) + config.BodyHeader = extractStringFromMap(messagesMap, "body-header", nil) return config } diff --git a/pkg/workflow/safe_outputs_messages_test.go b/pkg/workflow/safe_outputs_messages_test.go index 8f3754077f1..edba6c4f54f 100644 --- a/pkg/workflow/safe_outputs_messages_test.go +++ b/pkg/workflow/safe_outputs_messages_test.go @@ -102,6 +102,31 @@ func TestSafeOutputsMessagesConfiguration(t *testing.T) { t.Error("Expected Messages to be nil when not configured") } }) + + t.Run("Should parse body-header message", func(t *testing.T) { + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "messages": map[string]any{ + "body-header": "> ⚠️ Generated by [{workflow_name}]({run_url}).", + }, + }, + } + + config := compiler.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected SafeOutputsConfig to be parsed") + } + + if config.Messages == nil { + t.Fatal("Expected Messages to be parsed") + } + + if config.Messages.BodyHeader != "> ⚠️ Generated by [{workflow_name}]({run_url})." { + t.Errorf("Expected BodyHeader to be custom template, got %q", config.Messages.BodyHeader) + } + }) } func TestSerializeMessagesConfig(t *testing.T) { @@ -192,4 +217,32 @@ func TestSerializeMessagesConfig(t *testing.T) { t.Errorf("Expected Footer to be preserved, got %q", parsed.Footer) } }) + + t.Run("Should serialize body-header to camelCase JSON key", func(t *testing.T) { + config := &SafeOutputMessagesConfig{ + BodyHeader: "> ⚠️ AI-generated content from {workflow_name}", + } + + result, err := serializeMessagesConfig(config) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + var parsed SafeOutputMessagesConfig + if err := json.Unmarshal([]byte(result), &parsed); err != nil { + t.Fatalf("Result is not valid JSON: %v", err) + } + + if parsed.BodyHeader != "> ⚠️ AI-generated content from {workflow_name}" { + t.Errorf("Expected BodyHeader to be preserved, got %q", parsed.BodyHeader) + } + + if !strings.Contains(result, `"bodyHeader"`) { + t.Errorf("Expected JSON to contain camelCase key 'bodyHeader', got: %s", result) + } + + if strings.Contains(result, `"BodyHeader"`) { + t.Errorf("Expected JSON to NOT contain PascalCase key 'BodyHeader', got: %s", result) + } + }) } From cd650ac5efff099014758b8147af41162e4522c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 23:59:04 +0000 Subject: [PATCH 2/2] fix: correct unshift order so caution appears before body header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using Array.unshift, the last call places content at the very beginning of the array. Fix the ordering in create_issue.cjs, create_discussion.cjs, and create_pull_request.cjs so that the body header is unshifted first and the detection caution is unshifted second, resulting in the correct order: caution → header → user content. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1769a58a-3232-4313-89ce-33a6b2cfc9ef Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_discussion.cjs | 13 +++++++------ actions/setup/js/create_issue.cjs | 13 +++++++------ actions/setup/js/create_pull_request.cjs | 14 ++++++++------ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 60c0a909b31..94ba26308ed 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -516,18 +516,19 @@ async function main(config = {}) { const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID || ""; const runUrl = buildWorkflowRunUrl(context, context.repo); + // Inject body header before user content (unshifted first, so caution will appear before it) + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + // Inject CAUTION at top of body if threat detection warning was raised + // (unshifted after header so it appears first in the final output) const detectionCaution = getDetectionCautionAlert(workflowName, runUrl); if (detectionCaution) { bodyLines.unshift(...detectionCaution.split("\n"), ""); } - // Inject body header after any caution alert, before user content - const bodyHeader = getBodyHeader({ workflowName, runUrl }); - if (bodyHeader) { - bodyLines.unshift(...bodyHeader.split("\n"), ""); - } - // Generate footer with expiration using helper // When footer is disabled, only add XML markers (no visible footer content) if (includeFooter) { diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index e321cb4b393..4cc87ba43bd 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -448,18 +448,19 @@ async function main(config = {}) { const callerWorkflowId = process.env.GH_AW_CALLER_WORKFLOW_ID ?? ""; const runUrl = buildWorkflowRunUrl(context, context.repo); + // Inject body header before user content (unshifted first, so caution will appear before it) + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + // Inject CAUTION at top of body if threat detection warning was raised + // (unshifted after header so it appears first in the final output) const detectionCaution = getDetectionCautionAlert(workflowName, runUrl); if (detectionCaution) { bodyLines.unshift(...detectionCaution.split("\n"), ""); } - // Inject body header after any caution alert, before user content - const bodyHeader = getBodyHeader({ workflowName, runUrl }); - if (bodyHeader) { - bodyLines.unshift(...bodyHeader.split("\n"), ""); - } - // Add tracker-id comment if present const trackerIDComment = getTrackerID("markdown"); if (trackerIDComment) { diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 5fd61922295..14da4a5ee1b 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1127,6 +1127,14 @@ async function main(config = {}) { // Prepend threat detection caution alert at the very top of the PR body so it is // immediately visible to reviewers. The caution is omitted from the footer to // avoid duplication (skipDetectionCaution is passed to generateFooterWithMessages). + + // Inject body header before user content (unshifted first, so caution will appear before it) + const bodyHeader = getBodyHeader({ workflowName, runUrl }); + if (bodyHeader) { + bodyLines.unshift(...bodyHeader.split("\n"), ""); + } + + // Inject CAUTION at top of body (unshifted after header so it appears first in the final output) const detectionCaution = getDetectionCautionAlert(workflowName, runUrl); if (detectionCaution) { // unshift(caution, "", "") places the caution alert at index 0 and two blank @@ -1134,12 +1142,6 @@ async function main(config = {}) { bodyLines.unshift(detectionCaution, "", ""); } - // Inject body header after any caution alert, before user content - const bodyHeader = getBodyHeader({ workflowName, runUrl }); - if (bodyHeader) { - bodyLines.unshift(...bodyHeader.split("\n"), ""); - } - // Add fingerprint comment if present const trackerIDComment = getTrackerID("markdown"); if (trackerIDComment) {