diff --git a/.changeset/patch-add-safe-output-body-footer.md b/.changeset/patch-add-safe-output-body-footer.md new file mode 100644 index 00000000000..140210a6324 --- /dev/null +++ b/.changeset/patch-add-safe-output-body-footer.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add deterministic `body-footer` templates to issue and pull request creation safe outputs. diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 95e91bdc3d2..fd96b6eec87 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -4,7 +4,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 { generateFooterWithMessages, getBodyFooterMessage, getDetectionCautionAlert } = require("./messages_footer.cjs"); const { getBodyHeader, getDisclosureHeader } = require("./messages_header.cjs"); const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, normalizeCloseOlderKey } = require("./generate_footer.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); @@ -30,6 +30,7 @@ const { MAX_LABELS, MAX_ASSIGNEES } = require("./constants.cjs"); const { findAgent, getIssueDetails, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } = require("./issue_title_dedup.cjs"); const { resolveAllowedMentionsFromPayload } = require("./resolve_mentions_from_payload.cjs"); +const MAX_GITHUB_BODY_LENGTH = 65536; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ISSUE_FIELD_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; const RECENTLY_CLOSED_DEDUP_DAYS = 30; @@ -1079,6 +1080,11 @@ async function main(config = {}) { bodyLines.push(``, footer); } + const bodyFooter = getBodyFooterMessage(config.body_footer, { workflowName, runUrl }); + if (bodyFooter) { + bodyLines.push(``, bodyFooter.trimEnd()); + } + // Add standalone workflow-id marker for searchability (consistent with comments) // Always add XML markers even when footer is disabled if (workflowId) { @@ -1188,6 +1194,9 @@ async function main(config = {}) { } try { + if (body.length > MAX_GITHUB_BODY_LENGTH) { + throw new Error(`${ERR_VALIDATION}: Issue body exceeds GitHub's maximum length of ${MAX_GITHUB_BODY_LENGTH} characters`); + } const { data: issue } = await withRetry( () => githubClient.rest.issues.create({ diff --git a/actions/setup/js/create_issue.test.cjs b/actions/setup/js/create_issue.test.cjs index c188ad99b15..7a5b5672086 100644 --- a/actions/setup/js/create_issue.test.cjs +++ b/actions/setup/js/create_issue.test.cjs @@ -126,6 +126,36 @@ describe("create_issue", () => { ); }); + it("should append the configured body footer when the generated footer is disabled", async () => { + const handler = await main({ + footer: false, + body_footer: "Required footer from {workflow_name}: {run_url}", + }); + await handler({ + title: "Test Issue", + body: "Test body content", + }); + + const createCall = mockGithub.rest.issues.create.mock.calls[0][0]; + expect(createCall.body).toContain("Test body content\n\nRequired footer from Test Workflow: https://github.com/test-owner/test-repo/actions/runs/12345"); + expect(createCall.body).not.toContain("> Generated by"); + expect(createCall.body.indexOf("Required footer from Test Workflow")).toBeLessThan(createCall.body.indexOf("