Skip to content

Commit 5c8d02b

Browse files
authored
Add footer boolean field to safe-output configurations (individual and global) (#15079)
1 parent 77f63b8 commit 5c8d02b

23 files changed

Lines changed: 505 additions & 25 deletions

actions/setup/js/create_discussion.cjs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ async function main(config = {}) {
178178
const expiresHours = config.expires ? parseInt(String(config.expires), 10) : 0;
179179
const fallbackToIssue = config.fallback_to_issue !== false; // Default to true
180180
const closeOlderDiscussions = config.close_older_discussions === true || config.close_older_discussions === "true";
181+
const includeFooter = config.footer !== false; // Default to true (include footer)
181182

182183
// Parse labels from config
183184
const labelsConfig = config.labels || [];
@@ -366,15 +367,18 @@ async function main(config = {}) {
366367
const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
367368

368369
// Generate footer with expiration using helper
369-
const footer = generateFooterWithExpiration({
370-
footerText: `> AI generated by [${workflowName}](${runUrl})`,
371-
expiresHours,
372-
entityType: "Discussion",
373-
});
374-
375-
bodyLines.push(``, ``, footer);
370+
// When footer is disabled, only add XML markers (no visible footer content)
371+
if (includeFooter) {
372+
const footer = generateFooterWithExpiration({
373+
footerText: `> AI generated by [${workflowName}](${runUrl})`,
374+
expiresHours,
375+
entityType: "Discussion",
376+
});
377+
bodyLines.push(``, ``, footer);
378+
}
376379

377380
// Add standalone workflow-id marker for searchability (consistent with comments)
381+
// Always add XML markers even when footer is disabled
378382
if (workflowId) {
379383
bodyLines.push(``, generateWorkflowIdMarker(workflowId));
380384
}

actions/setup/js/create_issue.cjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ async function main(config = {}) {
198198
const defaultTargetRepo = getDefaultTargetRepo(config);
199199
const groupEnabled = config.group === true || config.group === "true";
200200
const closeOlderIssuesEnabled = config.close_older_issues === true || config.close_older_issues === "true";
201+
const includeFooter = config.footer !== false; // Default to true (include footer)
201202

202203
// Check if copilot assignment is enabled
203204
const assignCopilot = process.env.GH_AW_ASSIGN_COPILOT === "true";
@@ -417,11 +418,14 @@ async function main(config = {}) {
417418
}
418419

419420
// Generate footer and add expiration using helper
420-
const footer = addExpirationToFooter(generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue");
421-
422-
bodyLines.push(``, ``, footer);
421+
// When footer is disabled, only add XML markers (no visible footer content)
422+
if (includeFooter) {
423+
const footer = addExpirationToFooter(generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue");
424+
bodyLines.push(``, ``, footer);
425+
}
423426

424427
// Add standalone workflow-id marker for searchability (consistent with comments)
428+
// Always add XML markers even when footer is disabled
425429
if (workflowId) {
426430
bodyLines.push(``, generateWorkflowIdMarker(workflowId));
427431
}

actions/setup/js/create_pull_request.cjs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ async function main(config = {}) {
7171
const baseBranch = config.base_branch || "";
7272
const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024;
7373
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
74+
const includeFooter = config.footer !== false; // Default to true (include footer)
7475

7576
// Environment validation - fail early if required variables are missing
7677
const workflowId = process.env.GH_AW_WORKFLOW_ID;
@@ -371,16 +372,19 @@ async function main(config = {}) {
371372
}
372373

373374
// Generate footer with expiration using helper
374-
const footer = generateFooterWithExpiration({
375-
footerText: `> AI generated by [${workflowName}](${runUrl})`,
376-
expiresHours,
377-
entityType: "Pull Request",
378-
suffix: expiresHours > 0 ? "\n\n<!-- gh-aw-expires-type: pull-request -->" : undefined,
379-
});
380-
381-
bodyLines.push(``, ``, footer);
375+
// When footer is disabled, only add XML markers (no visible footer content)
376+
if (includeFooter) {
377+
const footer = generateFooterWithExpiration({
378+
footerText: `> AI generated by [${workflowName}](${runUrl})`,
379+
expiresHours,
380+
entityType: "Pull Request",
381+
suffix: expiresHours > 0 ? "\n\n<!-- gh-aw-expires-type: pull-request -->" : undefined,
382+
});
383+
bodyLines.push(``, ``, footer);
384+
}
382385

383386
// Add standalone workflow-id marker for searchability (consistent with comments)
387+
// Always add XML markers even when footer is disabled
384388
if (workflowId) {
385389
bodyLines.push(``, generateWorkflowIdMarker(workflowId));
386390
}

actions/setup/js/types/safe-outputs-config.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface CreateIssueConfig extends SafeOutputConfig {
1616
labels?: string[];
1717
"target-repo"?: string;
1818
"allowed-repos"?: string[];
19+
footer?: boolean;
1920
}
2021

2122
/**
@@ -26,6 +27,7 @@ interface CreateDiscussionConfig extends SafeOutputConfig {
2627
"category-id"?: string;
2728
"target-repo"?: string;
2829
"allowed-repos"?: string[];
30+
footer?: boolean;
2931
}
3032

3133
/**
@@ -81,6 +83,7 @@ interface CreatePullRequestConfig extends SafeOutputConfig {
8183
labels?: string[];
8284
draft?: boolean;
8385
"if-no-changes"?: string;
86+
footer?: boolean;
8487
}
8588

8689
/**
@@ -128,6 +131,7 @@ interface UpdateIssueConfig extends SafeOutputConfig {
128131
target?: string;
129132
title?: boolean;
130133
body?: boolean;
134+
footer?: boolean;
131135
}
132136

133137
/**
@@ -137,6 +141,7 @@ interface UpdateDiscussionConfig extends SafeOutputConfig {
137141
target?: string;
138142
title?: boolean;
139143
body?: boolean;
144+
footer?: boolean;
140145
}
141146

142147
/**
@@ -190,6 +195,7 @@ interface AssignToAgentConfig extends SafeOutputConfig {
190195
*/
191196
interface UpdateReleaseConfig extends SafeOutputConfig {
192197
target?: string;
198+
footer?: boolean;
193199
}
194200

195201
/**

actions/setup/js/update_discussion.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ function buildDiscussionUpdateData(item, config) {
134134
updateData.body = item.body;
135135
}
136136

137+
// Pass footer config to executeUpdate (default to true)
138+
updateData._includeFooter = config.footer !== false;
139+
137140
return { success: true, data: updateData };
138141
}
139142

actions/setup/js/update_issue.cjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
2727
// Default to "append" to add footer with AI attribution
2828
const operation = updateData._operation || "append";
2929
let rawBody = updateData._rawBody;
30+
const includeFooter = updateData._includeFooter !== false; // Default to true
3031

3132
// Remove internal fields
32-
const { _operation, _rawBody, ...apiData } = updateData;
33+
const { _operation, _rawBody, _includeFooter, ...apiData } = updateData;
3334

3435
// If we have a body, process it with the appropriate operation
3536
if (rawBody !== undefined) {
@@ -61,6 +62,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
6162
workflowName,
6263
runUrl,
6364
runId: context.runId,
65+
includeFooter, // Pass footer flag to helper
6466
});
6567

6668
core.info(`Will update body (length: ${apiData.body.length})`);
@@ -136,6 +138,9 @@ function buildIssueUpdateData(item, config) {
136138
updateData.milestone = item.milestone;
137139
}
138140

141+
// Pass footer config to executeUpdate (default to true)
142+
updateData._includeFooter = config.footer !== false;
143+
139144
return { success: true, data: updateData };
140145
}
141146

actions/setup/js/update_pr_description_helpers.cjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,16 @@ function findIsland(body, runId) {
7171
* @param {string} params.workflowName - Name of the workflow
7272
* @param {string} params.runUrl - URL of the workflow run
7373
* @param {number} params.runId - Workflow run ID
74+
* @param {boolean} [params.includeFooter=true] - Whether to include AI-generated footer (default: true)
7475
* @returns {string} Updated body content
7576
*/
7677
function updateBody(params) {
77-
const { currentBody, newContent, operation, workflowName, runUrl, runId } = params;
78-
const aiFooter = buildAIFooter(workflowName, runUrl);
78+
const { currentBody, newContent, operation, workflowName, runUrl, runId, includeFooter = true } = params;
79+
const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : "";
7980

8081
if (operation === "replace") {
81-
// Replace: use new content with AI footer
82-
core.info("Operation: replace (full body replacement with footer)");
82+
// Replace: use new content with optional AI footer
83+
core.info("Operation: replace (full body replacement)");
8384
return newContent + aiFooter;
8485
}
8586

@@ -109,7 +110,7 @@ function updateBody(params) {
109110
}
110111

111112
if (operation === "prepend") {
112-
// Prepend: add content, AI footer, and horizontal line at the start
113+
// Prepend: add content, AI footer (if enabled), and horizontal line at the start
113114
core.info("Operation: prepend (add to start with separator)");
114115
const prependSection = `${newContent}${aiFooter}\n\n---\n\n`;
115116
return prependSection + currentBody;

actions/setup/js/update_pr_description_helpers.test.cjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,100 @@ describe("update_pr_description_helpers.cjs", () => {
400400
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("append"));
401401
});
402402
});
403+
404+
describe("Footer parameter", () => {
405+
it("should omit footer when includeFooter is false", () => {
406+
const result = updateBody({
407+
currentBody: "Original",
408+
newContent: "New content",
409+
operation: "append",
410+
workflowName: "Test",
411+
runUrl: "https://github.com/test/actions/runs/123",
412+
runId: 123,
413+
includeFooter: false,
414+
});
415+
expect(result).toContain("Original");
416+
expect(result).toContain("New content");
417+
expect(result).not.toContain("Generated by");
418+
expect(result).not.toContain("AI generated");
419+
});
420+
421+
it("should include footer by default when includeFooter is not specified", () => {
422+
const result = updateBody({
423+
currentBody: "Original",
424+
newContent: "New content",
425+
operation: "append",
426+
workflowName: "Test",
427+
runUrl: "https://github.com/test/actions/runs/123",
428+
runId: 123,
429+
// includeFooter not specified, should default to true
430+
});
431+
expect(result).toContain("Original");
432+
expect(result).toContain("New content");
433+
expect(result).toContain("Generated by");
434+
});
435+
436+
it("should include footer when includeFooter is explicitly true", () => {
437+
const result = updateBody({
438+
currentBody: "Original",
439+
newContent: "New content",
440+
operation: "append",
441+
workflowName: "Test",
442+
runUrl: "https://github.com/test/actions/runs/123",
443+
runId: 123,
444+
includeFooter: true,
445+
});
446+
expect(result).toContain("Original");
447+
expect(result).toContain("New content");
448+
expect(result).toContain("Generated by");
449+
});
450+
451+
it("should omit footer in replace operation when includeFooter is false", () => {
452+
const result = updateBody({
453+
currentBody: "Original",
454+
newContent: "Replacement",
455+
operation: "replace",
456+
workflowName: "Test",
457+
runUrl: "https://github.com/test/actions/runs/123",
458+
runId: 123,
459+
includeFooter: false,
460+
});
461+
expect(result).toBe("Replacement");
462+
expect(result).not.toContain("Generated by");
463+
});
464+
465+
it("should omit footer in prepend operation when includeFooter is false", () => {
466+
const result = updateBody({
467+
currentBody: "Original",
468+
newContent: "Prepended",
469+
operation: "prepend",
470+
workflowName: "Test",
471+
runUrl: "https://github.com/test/actions/runs/123",
472+
runId: 123,
473+
includeFooter: false,
474+
});
475+
expect(result).toContain("Prepended");
476+
expect(result).toContain("Original");
477+
expect(result).not.toContain("Generated by");
478+
});
479+
480+
it("should omit footer in replace-island operation when includeFooter is false", () => {
481+
const currentBody = "Before\n<!-- gh-aw-island-start:123 -->\nOld island\n<!-- gh-aw-island-end:123 -->\nAfter";
482+
const result = updateBody({
483+
currentBody,
484+
newContent: "New island",
485+
operation: "replace-island",
486+
workflowName: "Test",
487+
runUrl: "https://github.com/test/actions/runs/123",
488+
runId: 123,
489+
includeFooter: false,
490+
});
491+
expect(result).toContain("Before");
492+
expect(result).toContain("New island");
493+
expect(result).toContain("After");
494+
expect(result).not.toContain("Generated by");
495+
expect(result).toContain("<!-- gh-aw-island-start:123 -->");
496+
expect(result).toContain("<!-- gh-aw-island-end:123 -->");
497+
});
498+
});
403499
});

actions/setup/js/update_release.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ const { updateBody } = require("./update_pr_description_helpers.cjs");
1010
*
1111
* @param {Object} config - Handler configuration
1212
* @param {number} [config.max] - Maximum number of releases to update
13+
* @param {boolean} [config.footer] - Controls whether AI-generated footer is added (default: true)
1314
* @returns {Promise<Function>} Handler function that processes a single message
1415
*/
1516
async function main(config = {}) {
1617
// Check if we're in staged mode
1718
const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true";
1819
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow";
20+
const includeFooter = config.footer !== false; // Default to true (include footer)
1921

2022
/**
2123
* Process a single update-release message
@@ -90,6 +92,7 @@ async function main(config = {}) {
9092
workflowName,
9193
runUrl,
9294
runId: context.runId,
95+
includeFooter, // Pass footer flag to helper
9396
});
9497

9598
// Update the release

0 commit comments

Comments
 (0)