From 374722768caaaa058555ff8bb2d2b5b82a1ee2d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 06:21:36 +0000 Subject: [PATCH 1/3] Initial plan From ec9a843d12d1a18d78375ec1180f3ffe53b7c3b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 06:27:26 +0000 Subject: [PATCH 2/3] chore: initial plan for disclosure-footer feature Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup-cli/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index c7a5ed2ffed..1c565c9e893 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -1,7 +1,7 @@ #!/bin/bash set +o histexpand -# Kept in sync with actions/setup-cli/install.sh — edit this file, then copy to that path. +# Script sync note: install-gh-aw.sh is canonical. actions/setup-cli/install.sh is copied from install-gh-aw.sh. # Script to download and install gh-aw binary for the current OS and architecture # Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) From 3f0e0ffaa9de1991d850ad9cf2f29cdb5fc84904 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 06:32:19 +0000 Subject: [PATCH 3/3] feat: add disclosure-footer built-in AI authorship disclosure for safe-outputs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/footers.md | 28 ++++ pkg/workflow/safe_outputs_config.go | 31 +++++ pkg/workflow/safe_outputs_messages_config.go | 5 + pkg/workflow/safe_outputs_messages_test.go | 129 +++++++++++++++++++ 4 files changed, 193 insertions(+) diff --git a/docs/src/content/docs/reference/footers.md b/docs/src/content/docs/reference/footers.md index 0718cda6144..261a12ece5f 100644 --- a/docs/src/content/docs/reference/footers.md +++ b/docs/src/content/docs/reference/footers.md @@ -71,6 +71,34 @@ The `footer` field accepts `"always"` (default), `"none"`, or `"if-body"` (foote The default value for `footer` is `true`. To hide footers, explicitly set `footer: false`. +## AI Authorship Disclosure Footer + +Use `disclosure-footer: true` to automatically append a standard AI authorship disclosure to every safe output (issues, PRs, discussions, comments). This is the recommended way to inform readers that content was AI-generated and posted under a maintainer's token: + +```yaml wrap +safe-outputs: + disclosure-footer: true + create-issue: + add-comment: +``` + +The built-in disclosure text is: + +``` +> 🤖 **Automated content by GitHub Copilot.** Posted via a maintainer's GitHub token, so it appears under their account — the account owner did **not** write or approve this content personally. Generated by the [{workflow_name}]({agentic_workflow_url}) workflow.{ai_credits_suffix} · [◷]({history_link}) +``` + +To use a custom disclosure template instead of the built-in text: + +```yaml wrap +safe-outputs: + disclosure-footer: + template: "> 🤖 AI-generated by [{workflow_name}]({agentic_workflow_url}).{ai_credits_suffix} · [◷]({history_link})" + create-issue: +``` + +`disclosure-footer` sets `messages.footer` under the hood. If you also set `messages.footer` explicitly, the explicit value takes precedence. + ## Customizing Footer Messages Instead of hiding footers entirely, you can customize the footer message text using the `messages.footer` template. This allows you to maintain attribution while using custom branding: diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index a8298a73874..2566070b0fd 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -579,6 +579,37 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut } } + // Handle disclosure-footer configuration (shorthand for standard AI authorship disclosure footer). + // Accepted shapes: + // - true: use the built-in standard disclosure footer text + // - {template: "..."}: use a custom disclosure footer text + // disclosure-footer only sets messages.footer when a footer template is not already explicitly + // provided via messages.footer, so an explicit messages.footer always takes precedence. + if disclosureFooter, exists := outputMap["disclosure-footer"]; exists { + switch v := disclosureFooter.(type) { + case bool: + if v { + if config.Messages == nil { + config.Messages = &SafeOutputMessagesConfig{} + } + if config.Messages.Footer == "" { + config.Messages.Footer = DefaultDisclosureFooter + safeOutputsConfigLog.Print("Applied built-in disclosure footer via disclosure-footer: true") + } + } + case map[string]any: + if tmpl, ok := v["template"].(string); ok && tmpl != "" { + if config.Messages == nil { + config.Messages = &SafeOutputMessagesConfig{} + } + if config.Messages.Footer == "" { + config.Messages.Footer = tmpl + safeOutputsConfigLog.Print("Applied custom disclosure footer via disclosure-footer.template") + } + } + } + } + // Handle activation-comments at safe-outputs top level (templatable boolean) if err := preprocessBoolFieldAsString(outputMap, "activation-comments", safeOutputsConfigLog); err != nil { safeOutputsConfigLog.Printf("activation-comments: %v", err) diff --git a/pkg/workflow/safe_outputs_messages_config.go b/pkg/workflow/safe_outputs_messages_config.go index c48abe36cab..138252282a2 100644 --- a/pkg/workflow/safe_outputs_messages_config.go +++ b/pkg/workflow/safe_outputs_messages_config.go @@ -9,6 +9,11 @@ import ( var safeOutputMessagesLog = logger.New("workflow:safe_outputs_config_messages") +// DefaultDisclosureFooter is the built-in AI authorship disclosure footer text used when +// disclosure-footer: true is set in safe-outputs frontmatter. It clearly identifies +// AI-generated content and explains that it was posted under a maintainer's token. +const DefaultDisclosureFooter = "> 🤖 **Automated content by GitHub Copilot.** Posted via a maintainer's GitHub token, so it appears under their account — the account owner did **not** write or approve this content personally. Generated by the [{workflow_name}]({agentic_workflow_url}) workflow.{ai_credits_suffix} · [◷]({history_link})" + // ======================================== // Safe Output Messages Configuration // ======================================== diff --git a/pkg/workflow/safe_outputs_messages_test.go b/pkg/workflow/safe_outputs_messages_test.go index edba6c4f54f..a4d8003f12e 100644 --- a/pkg/workflow/safe_outputs_messages_test.go +++ b/pkg/workflow/safe_outputs_messages_test.go @@ -129,6 +129,135 @@ func TestSafeOutputsMessagesConfiguration(t *testing.T) { }) } +func TestDisclosureFooter(t *testing.T) { + compiler := NewCompiler() + + t.Run("disclosure-footer: true sets built-in standard footer", func(t *testing.T) { + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "disclosure-footer": true, + }, + } + + config := compiler.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected SafeOutputsConfig to be parsed") + } + + if config.Messages == nil { + t.Fatal("Expected Messages to be non-nil when disclosure-footer is true") + } + + if config.Messages.Footer != DefaultDisclosureFooter { + t.Errorf("Expected Footer to be the built-in disclosure footer, got %q", config.Messages.Footer) + } + }) + + t.Run("disclosure-footer: true does not override explicit messages.footer", func(t *testing.T) { + customFooter := "> Custom footer by [{workflow_name}]({run_url})" + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "disclosure-footer": true, + "messages": map[string]any{ + "footer": customFooter, + }, + }, + } + + 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.Footer != customFooter { + t.Errorf("Expected explicit messages.footer to win over disclosure-footer, got %q", config.Messages.Footer) + } + }) + + t.Run("disclosure-footer: false does not set footer", func(t *testing.T) { + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "disclosure-footer": false, + }, + } + + config := compiler.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected SafeOutputsConfig to be parsed") + } + + if config.Messages != nil && config.Messages.Footer != "" { + t.Errorf("Expected no footer when disclosure-footer is false, got %q", config.Messages.Footer) + } + }) + + t.Run("disclosure-footer with template sets custom footer", func(t *testing.T) { + customTemplate := "> 🤖 AI-generated by [{workflow_name}]({run_url})" + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "disclosure-footer": map[string]any{ + "template": customTemplate, + }, + }, + } + + config := compiler.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected SafeOutputsConfig to be parsed") + } + + if config.Messages == nil { + t.Fatal("Expected Messages to be non-nil when disclosure-footer.template is set") + } + + if config.Messages.Footer != customTemplate { + t.Errorf("Expected Footer to be the custom template, got %q", config.Messages.Footer) + } + }) + + t.Run("disclosure-footer template does not override explicit messages.footer", func(t *testing.T) { + customFooter := "> Explicit footer" + customTemplate := "> Disclosure template" + frontmatter := map[string]any{ + "name": "Test Workflow", + "safe-outputs": map[string]any{ + "create-issue": nil, + "disclosure-footer": map[string]any{ + "template": customTemplate, + }, + "messages": map[string]any{ + "footer": customFooter, + }, + }, + } + + 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.Footer != customFooter { + t.Errorf("Expected explicit messages.footer to win over disclosure-footer template, got %q", config.Messages.Footer) + } + }) +} + func TestSerializeMessagesConfig(t *testing.T) { t.Run("Should serialize nil config to empty string", func(t *testing.T) { result, err := serializeMessagesConfig(nil)