Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions actions/setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
*/

const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { generateFooterWithMessages, generateXMLMarker } = require("./messages_footer.cjs");
const { getRepositoryUrl } = require("./get_repository_url.cjs");
const { replaceTemporaryIdReferences, loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
Expand Down Expand Up @@ -302,6 +302,7 @@ async function main(config = {}) {
const commentTarget = config.target || "triggering";
const maxCount = config.max || 20;
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
const includeFooter = parseBoolTemplatable(config.footer, true);

// Create an authenticated GitHub client. Uses config["github-token"] when set
// (for cross-repository operations), otherwise falls back to the step-level github.
Expand Down Expand Up @@ -524,8 +525,13 @@ async function main(config = {}) {
const triggeringPRNumber = context.payload.pull_request?.number;
const triggeringDiscussionNumber = context.payload.discussion?.number;

// Use generateFooterWithMessages to respect custom footer configuration
processedBody += generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd();
if (includeFooter) {
// When footer is enabled, add full footer with attribution and XML markers
processedBody += generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd();
} else {
// When footer is disabled, only add XML marker for searchability (no visible attribution text)
processedBody += "\n\n" + generateXMLMarker(workflowName, runUrl);
}
Comment on lines +531 to +534
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When footer: false, the other handlers (create_issue.cjs, create_pull_request.cjs, create_discussion.cjs) consistently add a generateWorkflowIdMarker (from generate_footer.cjs), which produces <!-- gh-aw-workflow-id: ${workflowId} -->. However, the new add_comment.cjs implementation calls generateXMLMarker (from messages_footer.cjs), which produces a much more complex <!-- gh-aw-agentic-workflow: ..., workflow_id: ..., run: ... --> marker.

This inconsistency also conflicts with the documentation added in faq.md (line 286), which explicitly states that <!-- gh-aw-workflow-id: ... --> will still be included for searchability. The actual marker format added by add_comment.cjs with footer: false uses gh-aw-agentic-workflow: as the prefix (not gh-aw-workflow-id:), and also includes the workflow_id: field only conditionally if GH_AW_WORKFLOW_ID is set.

To be consistent with other handlers and with the documentation, the footer: false path in add_comment.cjs should use the same approach as create_issue.cjs and create_pull_request.cjs: check if (workflowId) and add generateWorkflowIdMarker(workflowId) (imported from generate_footer.cjs).

Copilot uses AI. Check for mistakes.

// Enforce max limits again after adding footer and metadata
// This ensures the final body (including generated content) doesn't exceed limits
Expand Down
27 changes: 27 additions & 0 deletions docs/src/content/docs/reference/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,33 @@ The easy way to fix this problem is to set a secret `GH_AW_CI_TRIGGER_TOKEN` wit

See [Triggering CI](/gh-aw/reference/triggering-ci/) for more details on how to configure workflows to run CI checks on PRs created by agentic workflows.

### How do I suppress the "Generated by..." text in workflow outputs?

When workflows create or update issues, pull requests, discussions, or post comments, they append a `> Generated by [Workflow Name](run_url) for issue #N` attribution line. Use `footer: false` to hide this visible text while preserving the hidden XML markers used for search and tracking.

**Hide footers globally** (all safe output types):

```yaml wrap
safe-outputs:
footer: false
add-comment:
create-issue:
title-prefix: "[ai] "
```

**Hide footers for specific output types only:**

```yaml wrap
safe-outputs:
footer: false # hide for all by default
create-pull-request:
footer: true # override: show footer for PRs only
```

Even with `footer: false`, the hidden `<!-- gh-aw-workflow-id: ... -->` XML marker is still included in the content for searchability - you can search GitHub for `"gh-aw-workflow-id: my-workflow" in:body` to find all items created by a workflow.

See [Footer Control](/gh-aw/reference/footers/) for complete documentation including per-handler overrides and PR review footer options.

## Workflow Design

### Should I focus on one workflow, or write many different ones?
Expand Down
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,12 @@ safe-outputs:
allowed-repos: ["org/repo1", "org/repo2"] # additional allowed repositories
hide-older-comments: true # hide previous comments from same workflow
allowed-reasons: [outdated] # restrict hiding reasons (optional)
footer: false # omit AI-generated footer (default: true)
```

> [!TIP]
> Use `footer: false` to suppress the "Generated by..." attribution line in posted comments. See [Footer Control](/gh-aw/reference/footers/) for global and per-handler options.

The author of the parent issue, PR, or discussion receiving the comment is automatically preserved as an allowed mention. This means `@username` references to the issue/PR/discussion author are not neutralized when the workflow posts a reply.

#### Hide Older Comments
Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5065,6 +5065,11 @@
"type": "boolean",
"description": "Controls whether the workflow requests pull-requests:write permission for add-comment and includes pull requests in the event trigger condition. Default: true (includes pull-requests:write). Set to false to disable pull request commenting."
},
"footer": {
"type": "boolean",
"description": "Controls whether AI-generated footer is added to the comment. When false, the visible footer content is omitted but XML markers (workflow-id, metadata) are still included for searchability. Defaults to true.",
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The footer field description for add-comment says "XML markers (workflow-id, metadata)" but the equivalent description for create_issue's footer field says "XML markers (workflow-id, tracker-id, metadata)". Since add_comment.cjs also adds a tracker-id marker (regardless of the footer setting), the description should mention "tracker-id" as well, to be accurate and consistent.

Suggested change
"description": "Controls whether AI-generated footer is added to the comment. When false, the visible footer content is omitted but XML markers (workflow-id, metadata) are still included for searchability. Defaults to true.",
"description": "Controls whether AI-generated footer is added to the comment. When false, the visible footer content is omitted but XML markers (workflow-id, tracker-id, metadata) are still included for searchability. Defaults to true.",

Copilot uses AI. Check for mistakes.
"default": true
},
"github-token": {
"$ref": "#/$defs/github_token",
"description": "GitHub token to use for this specific output type. Overrides global github-token if specified."
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/add_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type AddCommentsConfig struct {
Issues *bool `yaml:"issues,omitempty"` // When false, excludes issues:write permission and issues from event condition. Default (nil or true) includes issues:write.
PullRequests *bool `yaml:"pull-requests,omitempty"` // When false, excludes pull-requests:write permission and PRs from event condition. Default (nil or true) includes pull-requests:write.
Discussions *bool `yaml:"discussions,omitempty"` // When false, excludes discussions:write permission and discussions from event condition. Default (nil or true) includes discussions:write.
Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept.
}

// buildCreateOutputAddCommentJob creates the add_comment job
Expand Down Expand Up @@ -162,6 +163,10 @@ func (c *Compiler) parseCommentsConfig(outputMap map[string]any) *AddCommentsCon
addCommentLog.Printf("Invalid hide-older-comments value: %v", err)
return nil
}
if err := preprocessBoolFieldAsString(configData, "footer", addCommentLog); err != nil {
addCommentLog.Printf("Invalid footer value: %v", err)
return nil
}

// Pre-process templatable int fields
if err := preprocessIntFieldAsString(configData, "max", addCommentLog); err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ var handlerRegistry = map[string]handlerBuilder{
AddIfNotEmpty("target-repo", c.TargetRepoSlug).
AddStringSlice("allowed_repos", c.AllowedRepos).
AddIfNotEmpty("github-token", c.GitHubToken).
AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)).
Build()
},
"create_discussion": func(cfg *SafeOutputsConfig) map[string]any {
Expand Down
61 changes: 61 additions & 0 deletions pkg/workflow/safe_outputs_footer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,71 @@ func TestFooterConfiguration(t *testing.T) {
assert.Equal(t, "false", *config.CreateIssues.Footer)
}

func TestAddCommentFooterConfiguration(t *testing.T) {
t.Run("footer: false on add-comment", func(t *testing.T) {
compiler := NewCompiler()
frontmatter := map[string]any{
"name": "Test",
"safe-outputs": map[string]any{
"add-comment": map[string]any{"footer": false},
},
}
config := compiler.extractSafeOutputsConfig(frontmatter)
require.NotNil(t, config)
require.NotNil(t, config.AddComments)
require.NotNil(t, config.AddComments.Footer)
assert.Equal(t, "false", *config.AddComments.Footer, "add-comment footer should be false")
})

t.Run("global footer: false propagates to add-comment", func(t *testing.T) {
compiler := NewCompiler()
frontmatter := map[string]any{
"name": "Test",
"safe-outputs": map[string]any{
"footer": false,
"add-comment": map[string]any{},
},
}
config := compiler.extractSafeOutputsConfig(frontmatter)
require.NotNil(t, config)
require.NotNil(t, config.Footer)
assert.False(t, *config.Footer, "Global footer should be false")

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)

addCommentConfig, ok := handlerConfig["add_comment"].(map[string]any)
require.True(t, ok, "add_comment handler config should exist")
assert.Equal(t, false, addCommentConfig["footer"], "add_comment should inherit global footer: false")
}
}
}
Comment on lines +66 to +82
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test in the "global footer: false propagates to add-comment" sub-test iterates over steps to find the GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG env var, but unlike the analogous TestGlobalFooterConfiguration test (line 116), there is no require.Contains guard before the loop to fail the test if no matching step is found. If addHandlerManagerConfigEnvVar emits no steps containing GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG, the loop body (including the require.True(t, ok, ...) assertion) is never reached, and the test passes vacuously. A guard like require.Contains(t, strings.Join(steps, ""), "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") should be added before the loop, as is done in the adjacent TestGlobalFooterConfiguration test.

Copilot uses AI. Check for mistakes.
})
}

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
"add-comment": map[string]any{},
"create-issue": map[string]any{"title-prefix": "[test] "},
"create-pull-request": nil,
"create-discussion": nil,
Expand Down Expand Up @@ -69,6 +127,9 @@ func TestGlobalFooterConfiguration(t *testing.T) {
require.NoError(t, err)

// All handlers should have footer: false from global setting
addCommentConfig, ok := handlerConfig["add_comment"].(map[string]any)
require.True(t, ok, "add_comment handler config should exist in global footer test")
assert.Equal(t, false, addCommentConfig["footer"], "add_comment should inherit global footer: false")
if issueConfig, ok := handlerConfig["create_issue"].(map[string]any); ok {
assert.Equal(t, false, issueConfig["footer"], "create_issue should inherit global footer: false")
}
Expand Down
Loading