From c5584400354f79c5f4bdc439ba7502fb6221cc0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:23:58 +0000 Subject: [PATCH 01/12] Initial plan From b1e373d1b981038b015929dbd481323780adf3b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:32:32 +0000 Subject: [PATCH 02/12] Add Footer field to safe-output configs and pass to JS handlers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_config.go | 6 ++++++ pkg/workflow/create_discussion.go | 7 +++++++ pkg/workflow/create_issue.go | 7 +++++++ pkg/workflow/create_pull_request.go | 7 +++++++ pkg/workflow/update_discussion.go | 1 + pkg/workflow/update_issue.go | 1 + pkg/workflow/update_release.go | 1 + 7 files changed, 30 insertions(+) diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 0fbc23757a..a177be2246 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -111,6 +111,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddIfTrue("group", c.Group). AddIfTrue("close_older_issues", c.CloseOlderIssues). + AddBoolPtr("footer", c.Footer). Build() }, "add_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -143,6 +144,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfPositive("expires", c.Expires). AddBoolPtr("fallback_to_issue", c.FallbackToIssue). AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddBoolPtr("footer", c.Footer). Build() }, "close_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -228,6 +230,7 @@ var handlerRegistry = map[string]handlerBuilder{ return builder. AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddBoolPtr("footer", c.Footer). Build() }, "update_discussion": func(cfg *SafeOutputsConfig) map[string]any { @@ -252,6 +255,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_labels", c.AllowedLabels). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddBoolPtr("footer", c.Footer). Build() }, "link_sub_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -276,6 +280,7 @@ var handlerRegistry = map[string]handlerBuilder{ c := cfg.UpdateRelease return newHandlerConfigBuilder(). AddIfPositive("max", c.Max). + AddBoolPtr("footer", c.Footer). Build() }, "create_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -313,6 +318,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddDefault("base_branch", "${{ github.ref_name }}"). AddDefault("max_patch_size", maxPatchSize). + AddBoolPtr("footer", c.Footer). Build() }, "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index 4e73c449c5..f7c68b6c93 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -23,6 +23,7 @@ type CreateDiscussionsConfig struct { RequiredCategory string `yaml:"required-category,omitempty"` // Required category for matching when close-older-discussions is enabled Expires int `yaml:"expires,omitempty"` // Hours until the discussion expires and should be automatically closed FallbackToIssue *bool `yaml:"fallback-to-issue,omitempty"` // When true (default), fallback to create-issue if discussion creation fails due to permissions + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // parseDiscussionsConfig handles create-discussion configuration @@ -143,6 +144,12 @@ func (c *Compiler) buildCreateOutputDiscussionJob(data *WorkflowData, mainJobNam customEnvVars = append(customEnvVars, " GH_AW_DISCUSSION_FALLBACK_TO_ISSUE: \"true\"\n") } + // Add footer flag if explicitly set to false + if data.SafeOutputs.CreateDiscussions.Footer != nil && !*data.SafeOutputs.CreateDiscussions.Footer { + customEnvVars = append(customEnvVars, " GH_AW_FOOTER: \"false\"\n") + discussionLog.Print("Footer disabled - XML markers will be included but visible footer content will be omitted") + } + // Add environment variable for temporary ID map from create_issue job if createIssueJobName != "" { customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TEMPORARY_ID_MAP: ${{ needs.%s.outputs.temporary_id_map }}\n", createIssueJobName)) diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 184a6ef02e..36b21f2212 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -20,6 +20,7 @@ type CreateIssuesConfig struct { CloseOlderIssues bool `yaml:"close-older-issues,omitempty"` // When true, close older issues with same title prefix or labels as "not planned" Expires int `yaml:"expires,omitempty"` // Hours until the issue expires and should be automatically closed Group bool `yaml:"group,omitempty"` // If true, group issues as sub-issues under a parent issue (workflow ID is used as group identifier) + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // parseIssuesConfig handles create-issue configuration @@ -154,6 +155,12 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str createIssueLog.Print("Close older issues enabled - older issues with same title prefix or labels will be closed") } + // Add footer flag if explicitly set to false + if data.SafeOutputs.CreateIssues.Footer != nil && !*data.SafeOutputs.CreateIssues.Footer { + customEnvVars = append(customEnvVars, " GH_AW_FOOTER: \"false\"\n") + createIssueLog.Print("Footer disabled - XML markers will be included but visible footer content will be omitted") + } + // Add standard environment variables (metadata + staged/target repo) customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, data.SafeOutputs.CreateIssues.TargetRepoSlug)...) diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 6dc109edf1..e4b1059f51 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -23,6 +23,7 @@ type CreatePullRequestsConfig struct { AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) AutoMerge bool `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // buildCreateOutputPullRequestJob creates the create_pull_request job @@ -105,6 +106,12 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_EXPIRES: \"%d\"\n", data.SafeOutputs.CreatePullRequests.Expires)) } + // Add footer flag if explicitly set to false + if data.SafeOutputs.CreatePullRequests.Footer != nil && !*data.SafeOutputs.CreatePullRequests.Footer { + customEnvVars = append(customEnvVars, " GH_AW_FOOTER: \"false\"\n") + createPRLog.Print("Footer disabled - XML markers will be included but visible footer content will be omitted") + } + // Add standard environment variables (metadata + staged/target repo) customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, data.SafeOutputs.CreatePullRequests.TargetRepoSlug)...) diff --git a/pkg/workflow/update_discussion.go b/pkg/workflow/update_discussion.go index 05688aee91..8239cc2a2d 100644 --- a/pkg/workflow/update_discussion.go +++ b/pkg/workflow/update_discussion.go @@ -13,6 +13,7 @@ type UpdateDiscussionsConfig struct { Body *bool `yaml:"body,omitempty"` // Allow updating discussion body - presence indicates field can be updated Labels *bool `yaml:"labels,omitempty"` // Allow updating discussion labels - presence indicates field can be updated AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // parseUpdateDiscussionsConfig handles update-discussion configuration diff --git a/pkg/workflow/update_issue.go b/pkg/workflow/update_issue.go index 6063022fab..20bbc69826 100644 --- a/pkg/workflow/update_issue.go +++ b/pkg/workflow/update_issue.go @@ -12,6 +12,7 @@ type UpdateIssuesConfig struct { Status *bool `yaml:"status,omitempty"` // Allow updating issue status (open/closed) - presence indicates field can be updated Title *bool `yaml:"title,omitempty"` // Allow updating issue title - presence indicates field can be updated Body *bool `yaml:"body,omitempty"` // Allow updating issue body - presence indicates field can be updated + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // parseUpdateIssuesConfig handles update-issue configuration diff --git a/pkg/workflow/update_release.go b/pkg/workflow/update_release.go index df1cfbf09a..55c795b4b2 100644 --- a/pkg/workflow/update_release.go +++ b/pkg/workflow/update_release.go @@ -9,6 +9,7 @@ var updateReleaseLog = logger.New("workflow:update_release") // UpdateReleaseConfig holds configuration for updating GitHub releases from agent output type UpdateReleaseConfig struct { UpdateEntityConfig `yaml:",inline"` + Footer *bool `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. } // parseUpdateReleaseConfig handles update-release configuration From 9b5de6375b12f698a59e636d4ce9460283fae5b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:36:26 +0000 Subject: [PATCH 03/12] Implement footer flag support in JavaScript handlers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_discussion.cjs | 18 ++++++++++------- actions/setup/js/create_issue.cjs | 10 +++++++--- actions/setup/js/create_pull_request.cjs | 20 +++++++++++-------- actions/setup/js/update_discussion.cjs | 3 +++ actions/setup/js/update_issue.cjs | 7 ++++++- .../js/update_pr_description_helpers.cjs | 9 +++++---- actions/setup/js/update_release.cjs | 2 ++ 7 files changed, 46 insertions(+), 23 deletions(-) diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 205ba0e401..0d6daf1424 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -178,6 +178,7 @@ async function main(config = {}) { const expiresHours = config.expires ? parseInt(String(config.expires), 10) : 0; const fallbackToIssue = config.fallback_to_issue !== false; // Default to true const closeOlderDiscussions = config.close_older_discussions === true || config.close_older_discussions === "true"; + const includeFooter = config.footer !== false; // Default to true (include footer) // Parse labels from config const labelsConfig = config.labels || []; @@ -366,15 +367,18 @@ async function main(config = {}) { const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; // Generate footer with expiration using helper - const footer = generateFooterWithExpiration({ - footerText: `> AI generated by [${workflowName}](${runUrl})`, - expiresHours, - entityType: "Discussion", - }); - - bodyLines.push(``, ``, footer); + // When footer is disabled, only add XML markers (no visible footer content) + if (includeFooter) { + const footer = generateFooterWithExpiration({ + footerText: `> AI generated by [${workflowName}](${runUrl})`, + expiresHours, + entityType: "Discussion", + }); + bodyLines.push(``, ``, footer); + } // Add standalone workflow-id marker for searchability (consistent with comments) + // Always add XML markers even when footer is disabled if (workflowId) { bodyLines.push(``, generateWorkflowIdMarker(workflowId)); } diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 06f06687e3..9ea30bb352 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -198,6 +198,7 @@ async function main(config = {}) { const defaultTargetRepo = getDefaultTargetRepo(config); const groupEnabled = config.group === true || config.group === "true"; const closeOlderIssuesEnabled = config.close_older_issues === true || config.close_older_issues === "true"; + const includeFooter = config.footer !== false; // Default to true (include footer) // Check if copilot assignment is enabled const assignCopilot = process.env.GH_AW_ASSIGN_COPILOT === "true"; @@ -417,11 +418,14 @@ async function main(config = {}) { } // Generate footer and add expiration using helper - const footer = addExpirationToFooter(generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue"); - - bodyLines.push(``, ``, footer); + // When footer is disabled, only add XML markers (no visible footer content) + if (includeFooter) { + const footer = addExpirationToFooter(generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue"); + bodyLines.push(``, ``, footer); + } // Add standalone workflow-id marker for searchability (consistent with comments) + // Always add XML markers even when footer is disabled if (workflowId) { bodyLines.push(``, generateWorkflowIdMarker(workflowId)); } diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 18dde670de..b3e2e47d3d 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -71,6 +71,7 @@ async function main(config = {}) { const baseBranch = config.base_branch || ""; const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024; const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); + const includeFooter = config.footer !== false; // Default to true (include footer) // Environment validation - fail early if required variables are missing const workflowId = process.env.GH_AW_WORKFLOW_ID; @@ -371,16 +372,19 @@ async function main(config = {}) { } // Generate footer with expiration using helper - const footer = generateFooterWithExpiration({ - footerText: `> AI generated by [${workflowName}](${runUrl})`, - expiresHours, - entityType: "Pull Request", - suffix: expiresHours > 0 ? "\n\n" : undefined, - }); - - bodyLines.push(``, ``, footer); + // When footer is disabled, only add XML markers (no visible footer content) + if (includeFooter) { + const footer = generateFooterWithExpiration({ + footerText: `> AI generated by [${workflowName}](${runUrl})`, + expiresHours, + entityType: "Pull Request", + suffix: expiresHours > 0 ? "\n\n" : undefined, + }); + bodyLines.push(``, ``, footer); + } // Add standalone workflow-id marker for searchability (consistent with comments) + // Always add XML markers even when footer is disabled if (workflowId) { bodyLines.push(``, generateWorkflowIdMarker(workflowId)); } diff --git a/actions/setup/js/update_discussion.cjs b/actions/setup/js/update_discussion.cjs index c06d94b28f..b81a8b3354 100644 --- a/actions/setup/js/update_discussion.cjs +++ b/actions/setup/js/update_discussion.cjs @@ -134,6 +134,9 @@ function buildDiscussionUpdateData(item, config) { updateData.body = item.body; } + // Pass footer config to executeUpdate (default to true) + updateData._includeFooter = config.footer !== false; + return { success: true, data: updateData }; } diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index 534c97c668..02bbfcd2ae 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -27,9 +27,10 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) { // Default to "append" to add footer with AI attribution const operation = updateData._operation || "append"; let rawBody = updateData._rawBody; + const includeFooter = updateData._includeFooter !== false; // Default to true // Remove internal fields - const { _operation, _rawBody, ...apiData } = updateData; + const { _operation, _rawBody, _includeFooter, ...apiData } = updateData; // If we have a body, process it with the appropriate operation if (rawBody !== undefined) { @@ -61,6 +62,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) { workflowName, runUrl, runId: context.runId, + includeFooter, // Pass footer flag to helper }); core.info(`Will update body (length: ${apiData.body.length})`); @@ -136,6 +138,9 @@ function buildIssueUpdateData(item, config) { updateData.milestone = item.milestone; } + // Pass footer config to executeUpdate (default to true) + updateData._includeFooter = config.footer !== false; + return { success: true, data: updateData }; } diff --git a/actions/setup/js/update_pr_description_helpers.cjs b/actions/setup/js/update_pr_description_helpers.cjs index 7f1390589c..efb8fb0ae5 100644 --- a/actions/setup/js/update_pr_description_helpers.cjs +++ b/actions/setup/js/update_pr_description_helpers.cjs @@ -71,14 +71,15 @@ function findIsland(body, runId) { * @param {string} params.workflowName - Name of the workflow * @param {string} params.runUrl - URL of the workflow run * @param {number} params.runId - Workflow run ID + * @param {boolean} [params.includeFooter=true] - Whether to include AI-generated footer (default: true) * @returns {string} Updated body content */ function updateBody(params) { - const { currentBody, newContent, operation, workflowName, runUrl, runId } = params; - const aiFooter = buildAIFooter(workflowName, runUrl); + const { currentBody, newContent, operation, workflowName, runUrl, runId, includeFooter = true } = params; + const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : ""; if (operation === "replace") { - // Replace: use new content with AI footer + // Replace: use new content with AI footer (if enabled) core.info("Operation: replace (full body replacement with footer)"); return newContent + aiFooter; } @@ -109,7 +110,7 @@ function updateBody(params) { } if (operation === "prepend") { - // Prepend: add content, AI footer, and horizontal line at the start + // Prepend: add content, AI footer (if enabled), and horizontal line at the start core.info("Operation: prepend (add to start with separator)"); const prependSection = `${newContent}${aiFooter}\n\n---\n\n`; return prependSection + currentBody; diff --git a/actions/setup/js/update_release.cjs b/actions/setup/js/update_release.cjs index 335135695d..e07d14fe71 100644 --- a/actions/setup/js/update_release.cjs +++ b/actions/setup/js/update_release.cjs @@ -16,6 +16,7 @@ async function main(config = {}) { // Check if we're in staged mode const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow"; + const includeFooter = config.footer !== false; // Default to true (include footer) /** * Process a single update-release message @@ -90,6 +91,7 @@ async function main(config = {}) { workflowName, runUrl, runId: context.runId, + includeFooter, // Pass footer flag to helper }); // Update the release From 7488335522ae2547aa7e322336e7454d2be2f12f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:41:55 +0000 Subject: [PATCH 04/12] Add footer field to update config parsers and schemas Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 30 ++++++++++++++++++++ pkg/workflow/update_discussion.go | 1 + pkg/workflow/update_issue.go | 1 + pkg/workflow/update_release.go | 4 ++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 10ec36bd20..c18daf23d3 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3942,6 +3942,11 @@ "type": "boolean", "description": "When true, automatically close older issues with the same workflow-id marker as 'not planned' with a comment linking to the new issue. Searches for issues containing the workflow-id marker in their body. Maximum 10 issues will be closed. Only runs if issue creation succeeds.", "default": false + }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added to the issue. When false, the visible footer content is omitted but XML markers (workflow-id, tracker-id, metadata) are still included for searchability. Defaults to true.", + "default": true } }, "additionalProperties": false, @@ -4364,6 +4369,11 @@ "description": "When true (default), fallback to creating an issue if discussion creation fails due to permissions. The fallback issue will include a note indicating it was intended to be a discussion. If close-older-discussions is enabled, the close-older-issues logic will be applied to the fallback issue.", "default": true }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added to the discussion. When false, the visible footer content is omitted but XML markers (workflow-id, tracker-id, metadata) are still included for searchability. Defaults to true.", + "default": true + }, "expires": { "oneOf": [ { @@ -4514,6 +4524,11 @@ "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository discussion updates. Takes precedence over trial target repo settings." + }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added when updating the discussion body. When false, the visible footer content is omitted. Defaults to true. Only applies when 'body' is enabled.", + "default": true } }, "additionalProperties": false @@ -4835,6 +4850,11 @@ "type": "boolean", "description": "Enable auto-merge for the pull request. When enabled, the PR will be automatically merged once all required checks pass and required approvals are met. Defaults to false.", "default": false + }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added to the pull request. When false, the visible footer content is omitted but XML markers (workflow-id, tracker-id, metadata) are still included for searchability. Defaults to true.", + "default": true } }, "additionalProperties": false, @@ -5290,6 +5310,11 @@ "type": "null", "description": "Allow updating issue body - presence of key indicates field can be updated" }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added when updating the issue body. When false, the visible footer content is omitted but XML markers are still included. Defaults to true. Only applies when 'body' is enabled.", + "default": true + }, "max": { "type": "integer", "description": "Maximum number of issues to update (default: 1)", @@ -5678,6 +5703,11 @@ "type": "string", "description": "Target repository for cross-repo release updates (format: owner/repo). If not specified, updates releases in the workflow's repository.", "pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$" + }, + "footer": { + "type": "boolean", + "description": "Controls whether AI-generated footer is added when updating the release body. When false, the visible footer content is omitted. Defaults to true.", + "default": true } }, "additionalProperties": false diff --git a/pkg/workflow/update_discussion.go b/pkg/workflow/update_discussion.go index 8239cc2a2d..f30bba41ab 100644 --- a/pkg/workflow/update_discussion.go +++ b/pkg/workflow/update_discussion.go @@ -25,6 +25,7 @@ func (c *Compiler) parseUpdateDiscussionsConfig(outputMap map[string]any) *Updat {Name: "title", Mode: FieldParsingKeyExistence, Dest: &cfg.Title}, {Name: "body", Mode: FieldParsingKeyExistence, Dest: &cfg.Body}, {Name: "labels", Mode: FieldParsingKeyExistence, Dest: &cfg.Labels}, + {Name: "footer", Mode: FieldParsingBoolValue, Dest: &cfg.Footer}, } }, func(cm map[string]any, cfg *UpdateDiscussionsConfig) { diff --git a/pkg/workflow/update_issue.go b/pkg/workflow/update_issue.go index 20bbc69826..752a2f0938 100644 --- a/pkg/workflow/update_issue.go +++ b/pkg/workflow/update_issue.go @@ -24,6 +24,7 @@ func (c *Compiler) parseUpdateIssuesConfig(outputMap map[string]any) *UpdateIssu {Name: "status", Mode: FieldParsingKeyExistence, Dest: &cfg.Status}, {Name: "title", Mode: FieldParsingKeyExistence, Dest: &cfg.Title}, {Name: "body", Mode: FieldParsingKeyExistence, Dest: &cfg.Body}, + {Name: "footer", Mode: FieldParsingBoolValue, Dest: &cfg.Footer}, } }, nil) } diff --git a/pkg/workflow/update_release.go b/pkg/workflow/update_release.go index 55c795b4b2..4784d861cb 100644 --- a/pkg/workflow/update_release.go +++ b/pkg/workflow/update_release.go @@ -17,6 +17,8 @@ func (c *Compiler) parseUpdateReleaseConfig(outputMap map[string]any) *UpdateRel return parseUpdateEntityConfigTyped(c, outputMap, UpdateEntityRelease, "update-release", updateReleaseLog, func(cfg *UpdateReleaseConfig) []UpdateEntityFieldSpec { - return nil // No entity-specific fields for releases + return []UpdateEntityFieldSpec{ + {Name: "footer", Mode: FieldParsingBoolValue, Dest: &cfg.Footer}, + } }, nil) } From 2455d4e9834670aa59ce6307f63d2c1b9031e2f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:50:36 +0000 Subject: [PATCH 05/12] Add tests for footer field configuration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/safe_outputs_footer_test.go | 60 ++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pkg/workflow/safe_outputs_footer_test.go diff --git a/pkg/workflow/safe_outputs_footer_test.go b/pkg/workflow/safe_outputs_footer_test.go new file mode 100644 index 0000000000..95ad1c2632 --- /dev/null +++ b/pkg/workflow/safe_outputs_footer_test.go @@ -0,0 +1,60 @@ +//go:build !integration + +package workflow + +import ( +"encoding/json" +"strings" +"testing" + +"github.com/stretchr/testify/assert" +"github.com/stretchr/testify/require" +) + +func TestFooterConfiguration(t *testing.T) { +compiler := NewCompiler() +frontmatter := map[string]any{ +"name": "Test", +"safe-outputs": map[string]any{ +"create-issue": map[string]any{"footer": false}, +}, +} +config := compiler.extractSafeOutputsConfig(frontmatter) +require.NotNil(t, config) +require.NotNil(t, config.CreateIssues) +require.NotNil(t, config.CreateIssues.Footer) +assert.False(t, *config.CreateIssues.Footer) +} + +func TestFooterInHandlerConfig(t *testing.T) { +compiler := NewCompiler() +workflowData := &WorkflowData{ +Name: "Test", +SafeOutputs: &SafeOutputsConfig{ +CreateIssues: &CreateIssuesConfig{ +BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, +Footer: boolPtr(false), +}, +}, +} +var steps []string +compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) +stepsContent := strings.Join(steps, "") +require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") +for _, step := range steps { +if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { +parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") +if len(parts) == 2 { +jsonStr := strings.TrimSpace(parts[1]) +jsonStr = strings.Trim(jsonStr, "\"") +jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") +var config map[string]any +err := json.Unmarshal([]byte(jsonStr), &config) +require.NoError(t, err) +issueConfig, ok := config["create_issue"].(map[string]any) +require.True(t, ok) +assert.Equal(t, false, issueConfig["footer"]) +} +} +} +} From 8284bc831691e8be4cb51f15c9e81655e89db0e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:02:01 +0000 Subject: [PATCH 06/12] Add TypeScript types and JSDoc for footer field Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/types/safe-outputs-config.d.ts | 6 ++ actions/setup/js/update_release.cjs | 1 + .../docs/reference/frontmatter-full.md | 35 +++++++ pkg/workflow/safe_outputs_footer_test.go | 94 +++++++++---------- 4 files changed, 89 insertions(+), 47 deletions(-) diff --git a/actions/setup/js/types/safe-outputs-config.d.ts b/actions/setup/js/types/safe-outputs-config.d.ts index f1eb310512..cb9918c224 100644 --- a/actions/setup/js/types/safe-outputs-config.d.ts +++ b/actions/setup/js/types/safe-outputs-config.d.ts @@ -16,6 +16,7 @@ interface CreateIssueConfig extends SafeOutputConfig { labels?: string[]; "target-repo"?: string; "allowed-repos"?: string[]; + footer?: boolean; } /** @@ -26,6 +27,7 @@ interface CreateDiscussionConfig extends SafeOutputConfig { "category-id"?: string; "target-repo"?: string; "allowed-repos"?: string[]; + footer?: boolean; } /** @@ -81,6 +83,7 @@ interface CreatePullRequestConfig extends SafeOutputConfig { labels?: string[]; draft?: boolean; "if-no-changes"?: string; + footer?: boolean; } /** @@ -128,6 +131,7 @@ interface UpdateIssueConfig extends SafeOutputConfig { target?: string; title?: boolean; body?: boolean; + footer?: boolean; } /** @@ -137,6 +141,7 @@ interface UpdateDiscussionConfig extends SafeOutputConfig { target?: string; title?: boolean; body?: boolean; + footer?: boolean; } /** @@ -190,6 +195,7 @@ interface AssignToAgentConfig extends SafeOutputConfig { */ interface UpdateReleaseConfig extends SafeOutputConfig { target?: string; + footer?: boolean; } /** diff --git a/actions/setup/js/update_release.cjs b/actions/setup/js/update_release.cjs index e07d14fe71..08611d3116 100644 --- a/actions/setup/js/update_release.cjs +++ b/actions/setup/js/update_release.cjs @@ -10,6 +10,7 @@ const { updateBody } = require("./update_pr_description_helpers.cjs"); * * @param {Object} config - Handler configuration * @param {number} [config.max] - Maximum number of releases to update + * @param {boolean} [config.footer] - Controls whether AI-generated footer is added (default: true) * @returns {Promise} Handler function that processes a single message */ async function main(config = {}) { diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index b0be7dc601..fd0dc167a5 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -1878,6 +1878,12 @@ safe-outputs: # (optional) close-older-issues: true + # Controls whether AI-generated footer is added to the issue. When false, the + # visible footer content is omitted but XML markers (workflow-id, tracker-id, + # metadata) are still included for searchability. Defaults to true. + # (optional) + footer: true + # Option 2: Enable issue creation with default configuration create-issue: null @@ -2222,6 +2228,12 @@ safe-outputs: # (optional) fallback-to-issue: true + # Controls whether AI-generated footer is added to the discussion. When false, the + # visible footer content is omitted but XML markers (workflow-id, tracker-id, + # metadata) are still included for searchability. Defaults to true. + # (optional) + footer: true + # Time until the discussion expires and should be automatically closed. Supports # integer (days), relative time format like '2h' (2 hours), '7d' (7 days), '2w' (2 # weeks), '1m' (1 month), '1y' (1 year), or false to disable expiration. Minimum @@ -2322,6 +2334,12 @@ safe-outputs: # (optional) target-repo: "example-value" + # Controls whether AI-generated footer is added when updating the discussion body. + # When false, the visible footer content is omitted. Defaults to true. Only + # applies when 'body' is enabled. + # (optional) + footer: true + # Option 2: Enable discussion updating with default configuration update-discussion: null @@ -2581,6 +2599,12 @@ safe-outputs: # (optional) auto-merge: true + # Controls whether AI-generated footer is added to the pull request. When false, + # the visible footer content is omitted but XML markers (workflow-id, tracker-id, + # metadata) are still included for searchability. Defaults to true. + # (optional) + footer: true + # Option 2: Enable pull request creation with default configuration create-pull-request: null @@ -2965,6 +2989,12 @@ safe-outputs: # (optional) body: null + # Controls whether AI-generated footer is added when updating the issue body. When + # false, the visible footer content is omitted but XML markers are still included. + # Defaults to true. Only applies when 'body' is enabled. + # (optional) + footer: true + # Maximum number of issues to update (default: 1) # (optional) max: 1 @@ -3274,6 +3304,11 @@ safe-outputs: # (optional) target-repo: "example-value" + # Controls whether AI-generated footer is added when updating the release body. + # When false, the visible footer content is omitted. Defaults to true. + # (optional) + footer: true + # Option 2: Enable release updates with default configuration update-release: null diff --git a/pkg/workflow/safe_outputs_footer_test.go b/pkg/workflow/safe_outputs_footer_test.go index 95ad1c2632..6979004cf9 100644 --- a/pkg/workflow/safe_outputs_footer_test.go +++ b/pkg/workflow/safe_outputs_footer_test.go @@ -3,58 +3,58 @@ package workflow import ( -"encoding/json" -"strings" -"testing" + "encoding/json" + "strings" + "testing" -"github.com/stretchr/testify/assert" -"github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFooterConfiguration(t *testing.T) { -compiler := NewCompiler() -frontmatter := map[string]any{ -"name": "Test", -"safe-outputs": map[string]any{ -"create-issue": map[string]any{"footer": false}, -}, -} -config := compiler.extractSafeOutputsConfig(frontmatter) -require.NotNil(t, config) -require.NotNil(t, config.CreateIssues) -require.NotNil(t, config.CreateIssues.Footer) -assert.False(t, *config.CreateIssues.Footer) + compiler := NewCompiler() + frontmatter := map[string]any{ + "name": "Test", + "safe-outputs": map[string]any{ + "create-issue": map[string]any{"footer": false}, + }, + } + config := compiler.extractSafeOutputsConfig(frontmatter) + require.NotNil(t, config) + require.NotNil(t, config.CreateIssues) + require.NotNil(t, config.CreateIssues.Footer) + assert.False(t, *config.CreateIssues.Footer) } func TestFooterInHandlerConfig(t *testing.T) { -compiler := NewCompiler() -workflowData := &WorkflowData{ -Name: "Test", -SafeOutputs: &SafeOutputsConfig{ -CreateIssues: &CreateIssuesConfig{ -BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, -Footer: boolPtr(false), -}, -}, -} -var steps []string -compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) -stepsContent := strings.Join(steps, "") -require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") -for _, step := range steps { -if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { -parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") -if len(parts) == 2 { -jsonStr := strings.TrimSpace(parts[1]) -jsonStr = strings.Trim(jsonStr, "\"") -jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") -var config map[string]any -err := json.Unmarshal([]byte(jsonStr), &config) -require.NoError(t, err) -issueConfig, ok := config["create_issue"].(map[string]any) -require.True(t, ok) -assert.Equal(t, false, issueConfig["footer"]) -} -} -} + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, + Footer: boolPtr(false), + }, + }, + } + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + stepsContent := strings.Join(steps, "") + require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + var config map[string]any + err := json.Unmarshal([]byte(jsonStr), &config) + require.NoError(t, err) + issueConfig, ok := config["create_issue"].(map[string]any) + require.True(t, ok) + assert.Equal(t, false, issueConfig["footer"]) + } + } + } } From cd52e698377fd11d662b248d7f1454c96d6b99ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:03:23 +0000 Subject: [PATCH 07/12] Add JavaScript tests for footer functionality Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../js/update_pr_description_helpers.test.cjs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/actions/setup/js/update_pr_description_helpers.test.cjs b/actions/setup/js/update_pr_description_helpers.test.cjs index 6d35008e37..61888854ed 100644 --- a/actions/setup/js/update_pr_description_helpers.test.cjs +++ b/actions/setup/js/update_pr_description_helpers.test.cjs @@ -400,4 +400,100 @@ describe("update_pr_description_helpers.cjs", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("append")); }); }); + + describe("Footer parameter", () => { + it("should omit footer when includeFooter is false", () => { + const result = updateBody({ + currentBody: "Original", + newContent: "New content", + operation: "append", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + includeFooter: false, + }); + expect(result).toContain("Original"); + expect(result).toContain("New content"); + expect(result).not.toContain("Generated by"); + expect(result).not.toContain("AI generated"); + }); + + it("should include footer by default when includeFooter is not specified", () => { + const result = updateBody({ + currentBody: "Original", + newContent: "New content", + operation: "append", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + // includeFooter not specified, should default to true + }); + expect(result).toContain("Original"); + expect(result).toContain("New content"); + expect(result).toContain("Generated by"); + }); + + it("should include footer when includeFooter is explicitly true", () => { + const result = updateBody({ + currentBody: "Original", + newContent: "New content", + operation: "append", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + includeFooter: true, + }); + expect(result).toContain("Original"); + expect(result).toContain("New content"); + expect(result).toContain("Generated by"); + }); + + it("should omit footer in replace operation when includeFooter is false", () => { + const result = updateBody({ + currentBody: "Original", + newContent: "Replacement", + operation: "replace", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + includeFooter: false, + }); + expect(result).toBe("Replacement"); + expect(result).not.toContain("Generated by"); + }); + + it("should omit footer in prepend operation when includeFooter is false", () => { + const result = updateBody({ + currentBody: "Original", + newContent: "Prepended", + operation: "prepend", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + includeFooter: false, + }); + expect(result).toContain("Prepended"); + expect(result).toContain("Original"); + expect(result).not.toContain("Generated by"); + }); + + it("should omit footer in replace-island operation when includeFooter is false", () => { + const currentBody = "Before\n\nOld island\n\nAfter"; + const result = updateBody({ + currentBody, + newContent: "New island", + operation: "replace-island", + workflowName: "Test", + runUrl: "https://github.com/test/actions/runs/123", + runId: 123, + includeFooter: false, + }); + expect(result).toContain("Before"); + expect(result).toContain("New island"); + expect(result).toContain("After"); + expect(result).not.toContain("Generated by"); + expect(result).toContain(""); + expect(result).toContain(""); + }); + }); }); From f60f27323949e5e7ac0e069fc401604ce248cb64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:06:28 +0000 Subject: [PATCH 08/12] Update comment to clarify operation behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/update_pr_description_helpers.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/update_pr_description_helpers.cjs b/actions/setup/js/update_pr_description_helpers.cjs index efb8fb0ae5..c4d993b13c 100644 --- a/actions/setup/js/update_pr_description_helpers.cjs +++ b/actions/setup/js/update_pr_description_helpers.cjs @@ -79,8 +79,8 @@ function updateBody(params) { const aiFooter = includeFooter ? buildAIFooter(workflowName, runUrl) : ""; if (operation === "replace") { - // Replace: use new content with AI footer (if enabled) - core.info("Operation: replace (full body replacement with footer)"); + // Replace: use new content with optional AI footer + core.info("Operation: replace (full body replacement)"); return newContent + aiFooter; } From 7ed20120b0e29dace30589b0e09f896081fb87c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:07:17 +0000 Subject: [PATCH 09/12] Add test workflow demonstrating footer: false functionality Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/workflows/test-footer-disabled.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pkg/cli/workflows/test-footer-disabled.md diff --git a/pkg/cli/workflows/test-footer-disabled.md b/pkg/cli/workflows/test-footer-disabled.md new file mode 100644 index 0000000000..9ae26841bf --- /dev/null +++ b/pkg/cli/workflows/test-footer-disabled.md @@ -0,0 +1,22 @@ +--- +on: + workflow_dispatch: +engine: copilot +permissions: + contents: read +safe-outputs: + create-issue: + title-prefix: "[test-footer] " + footer: false +--- + +# Test Footer Disabled in Create Issue + +Create a test issue with `footer: false` to demonstrate that: +1. The visible AI-generated footer is omitted +2. XML markers (workflow-id, tracker-id) are still included +3. The issue is searchable via workflow-id + +Create an issue with: +- **Title**: "Test issue without footer" +- **Body**: "This issue should not have a visible AI-generated footer, but should still have XML markers for searchability." From 3c0a4ab455ba61389d17694095438d1afedd3f1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:55:35 +0000 Subject: [PATCH 10/12] Changes before error encountered Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 6 ++++++ pkg/workflow/compiler_safe_outputs_config.go | 22 ++++++++++++++------ pkg/workflow/compiler_types.go | 1 + pkg/workflow/safe_outputs_config.go | 8 +++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index c18daf23d3..71e0bab2a5 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -6058,6 +6058,12 @@ } ] }, + "footer": { + "type": "boolean", + "description": "Global footer control for all safe outputs. When false, omits visible AI-generated footer content from all created/updated entities (issues, PRs, discussions, releases) while still including XML markers for searchability. Individual safe-output types (create-issue, update-issue, etc.) can override this by specifying their own footer field. Defaults to true.", + "default": true, + "examples": [false, true] + }, "runs-on": { "type": "string", "description": "Runner specification for all safe-outputs jobs (activation, create-issue, add-comment, etc.). Single runner label (e.g., 'ubuntu-slim', 'ubuntu-latest', 'windows-latest', 'self-hosted'). Defaults to 'ubuntu-slim'. See https://github.blog/changelog/2025-10-28-1-vcpu-linux-runner-now-available-in-github-actions-in-public-preview/" diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index a177be2246..f4dcdd6882 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -9,6 +9,16 @@ import ( var compilerSafeOutputsConfigLog = logger.New("workflow:compiler_safe_outputs_config") +// getEffectiveFooter returns the effective footer value for a config +// If the local footer is set, use it; otherwise fall back to global footer +// Returns nil if neither is set (default to true in JavaScript) +func getEffectiveFooter(localFooter *bool, globalFooter *bool) *bool { + if localFooter != nil { + return localFooter + } + return globalFooter +} + // handlerConfigBuilder provides a fluent API for building handler configurations type handlerConfigBuilder struct { config map[string]any @@ -111,7 +121,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddIfTrue("group", c.Group). AddIfTrue("close_older_issues", c.CloseOlderIssues). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "add_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -144,7 +154,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfPositive("expires", c.Expires). AddBoolPtr("fallback_to_issue", c.FallbackToIssue). AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "close_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -230,7 +240,7 @@ var handlerRegistry = map[string]handlerBuilder{ return builder. AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "update_discussion": func(cfg *SafeOutputsConfig) map[string]any { @@ -255,7 +265,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_labels", c.AllowedLabels). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "link_sub_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -280,7 +290,7 @@ var handlerRegistry = map[string]handlerBuilder{ c := cfg.UpdateRelease return newHandlerConfigBuilder(). AddIfPositive("max", c.Max). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "create_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -318,7 +328,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddDefault("base_branch", "${{ github.ref_name }}"). AddDefault("max_patch_size", maxPatchSize). - AddBoolPtr("footer", c.Footer). + AddBoolPtr("footer", getEffectiveFooter(c.Footer, cfg.Footer)). Build() }, "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 2a5d51091f..fc5570e9cd 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -502,6 +502,7 @@ type SafeOutputsConfig struct { RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs + Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) } // SafeOutputMessagesConfig holds custom message templates for safe-output footer and notification messages diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 1ba1a52d09..13b8ac3fb6 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -355,6 +355,14 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut config.Mentions = parseMentionsConfig(mentions) } + // Handle global footer flag + if footer, exists := outputMap["footer"]; exists { + if footerBool, ok := footer.(bool); ok { + config.Footer = &footerBool + safeOutputsConfigLog.Printf("Global footer control: %t", footerBool) + } + } + // Handle jobs (safe-jobs must be under safe-outputs) if jobs, exists := outputMap["jobs"]; exists { if jobsMap, ok := jobs.(map[string]any); ok { From ba5fe1f99efac4bda277ebec36b11bf7b7102e4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:44:48 +0000 Subject: [PATCH 11/12] Add global footer control and tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../docs/reference/frontmatter-full.md | 8 ++ pkg/workflow/safe_outputs_footer_test.go | 114 ++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index fd0dc167a5..7959d57818 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -3502,6 +3502,14 @@ safe-outputs: # (optional) max: 1 + # Global footer control for all safe outputs. When false, omits visible + # AI-generated footer content from all created/updated entities (issues, PRs, + # discussions, releases) while still including XML markers for searchability. + # Individual safe-output types (create-issue, update-issue, etc.) can override + # this by specifying their own footer field. Defaults to true. + # (optional) + footer: true + # Runner specification for all safe-outputs jobs (activation, create-issue, # add-comment, etc.). Single runner label (e.g., 'ubuntu-slim', 'ubuntu-latest', # 'windows-latest', 'self-hosted'). Defaults to 'ubuntu-slim'. See diff --git a/pkg/workflow/safe_outputs_footer_test.go b/pkg/workflow/safe_outputs_footer_test.go index 6979004cf9..a2184bab36 100644 --- a/pkg/workflow/safe_outputs_footer_test.go +++ b/pkg/workflow/safe_outputs_footer_test.go @@ -26,6 +26,120 @@ func TestFooterConfiguration(t *testing.T) { assert.False(t, *config.CreateIssues.Footer) } +func TestGlobalFooterConfiguration(t *testing.T) { + t.Run("global footer: false applies to all handlers", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "name": "Test", + "safe-outputs": map[string]any{ + "footer": false, // Global footer control + "create-issue": map[string]any{"title-prefix": "[test] "}, + "create-pull-request": nil, + "create-discussion": nil, + "update-issue": map[string]any{"body": nil}, + "update-discussion": map[string]any{"body": nil}, + "update-release": nil, + }, + } + config := compiler.extractSafeOutputsConfig(frontmatter) + require.NotNil(t, config) + require.NotNil(t, config.Footer) + assert.False(t, *config.Footer) + + // Verify global footer is propagated to handlers + workflowData := &WorkflowData{ + Name: "Test", + SafeOutputs: config, + } + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + stepsContent := strings.Join(steps, "") + require.Contains(t, stepsContent, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") + + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + var handlerConfig map[string]any + err := json.Unmarshal([]byte(jsonStr), &handlerConfig) + require.NoError(t, err) + + // All handlers should have footer: false from global setting + if issueConfig, ok := handlerConfig["create_issue"].(map[string]any); ok { + assert.Equal(t, false, issueConfig["footer"], "create_issue should inherit global footer: false") + } + if prConfig, ok := handlerConfig["create_pull_request"].(map[string]any); ok { + assert.Equal(t, false, prConfig["footer"], "create_pull_request should inherit global footer: false") + } + if discussionConfig, ok := handlerConfig["create_discussion"].(map[string]any); ok { + assert.Equal(t, false, discussionConfig["footer"], "create_discussion should inherit global footer: false") + } + if updateIssueConfig, ok := handlerConfig["update_issue"].(map[string]any); ok { + assert.Equal(t, false, updateIssueConfig["footer"], "update_issue should inherit global footer: false") + } + if updateDiscussionConfig, ok := handlerConfig["update_discussion"].(map[string]any); ok { + assert.Equal(t, false, updateDiscussionConfig["footer"], "update_discussion should inherit global footer: false") + } + if updateReleaseConfig, ok := handlerConfig["update_release"].(map[string]any); ok { + assert.Equal(t, false, updateReleaseConfig["footer"], "update_release should inherit global footer: false") + } + } + } + } + }) + + t.Run("local footer overrides global footer", func(t *testing.T) { + compiler := NewCompiler() + frontmatter := map[string]any{ + "name": "Test", + "safe-outputs": map[string]any{ + "footer": false, // Global: hide footer + "create-issue": map[string]any{"title-prefix": "[test] "}, + "create-pull-request": map[string]any{"footer": true}, // Local: show footer + }, + } + config := compiler.extractSafeOutputsConfig(frontmatter) + require.NotNil(t, config) + require.NotNil(t, config.Footer) + assert.False(t, *config.Footer, "Global footer should be false") + require.NotNil(t, config.CreatePullRequests.Footer) + assert.True(t, *config.CreatePullRequests.Footer, "Local PR footer should override to true") + + // Verify in handler config + workflowData := &WorkflowData{ + Name: "Test", + SafeOutputs: config, + } + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + var handlerConfig map[string]any + err := json.Unmarshal([]byte(jsonStr), &handlerConfig) + require.NoError(t, err) + + issueConfig, ok := handlerConfig["create_issue"].(map[string]any) + require.True(t, ok) + assert.Equal(t, false, issueConfig["footer"], "create_issue should use global footer: false") + + prConfig, ok := handlerConfig["create_pull_request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, true, prConfig["footer"], "create_pull_request should override to footer: true") + } + } + } + }) +} + func TestFooterInHandlerConfig(t *testing.T) { compiler := NewCompiler() workflowData := &WorkflowData{ From 0f3fdb29a094e0b364f88794300713d6d06a9edf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:46:11 +0000 Subject: [PATCH 12/12] Add test workflows for global footer control Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../workflows/test-global-footer-override.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pkg/cli/workflows/test-global-footer-override.md diff --git a/pkg/cli/workflows/test-global-footer-override.md b/pkg/cli/workflows/test-global-footer-override.md new file mode 100644 index 0000000000..3597d99d4d --- /dev/null +++ b/pkg/cli/workflows/test-global-footer-override.md @@ -0,0 +1,26 @@ +--- +on: + workflow_dispatch: +engine: copilot +permissions: + contents: read +safe-outputs: + footer: false # Global: hide footer for all safe outputs + create-issue: + title-prefix: "[global-off] " + # Uses global footer: false + create-pull-request: + title-prefix: "[override-on] " + footer: true # Local override: show footer for PRs only +--- + +# Test Global Footer with Override + +Demonstrates global footer control with local override: + +1. **Global setting**: `safe-outputs.footer: false` hides footers for all outputs +2. **Local override**: `create-pull-request.footer: true` shows footer only for PRs + +Create two outputs to demonstrate: +- An issue with title "[global-off] Test issue" (no footer) +- A note that if this were creating a PR, it would have a footer due to the override