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
1 change: 0 additions & 1 deletion pkg/workflow/add_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ 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 true, includes discussions:write permission. Default (nil or false) excludes 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.
}

// parseCommentsConfig handles add-comment configuration
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/comment_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type CommentMemoryConfig struct {
TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in owner/repo format
AllowedRepos []string `yaml:"allowed-repos,omitempty"` // Additional allowed repositories
MemoryID string `yaml:"memory-id,omitempty"` // Default memory identifier when item does not provide memory_id
Footer *string `yaml:"footer,omitempty"` // Footer visibility control ("true"/"false" templatable string); nil defaults to visible footer
}

const commentMemoryHandlerKey = "comment_memory"
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/comment_memory_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ func TestCommentMemoryToolConfig(t *testing.T) {
`,
expectedCommentMemory: &CommentMemoryConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: strPtr("3"),
Max: strPtr("3"),
Footer: strPtr("false"),
},
MemoryID: "triage",
Target: "42",
TargetRepoSlug: "github/docs",
AllowedRepos: []string{"github/docs", "github/gh-aw"},
Footer: strPtr("false"),
},
},
}
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/create_discussion.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ 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 *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept.
}

// parseCreateDiscussionsConfig handles create-discussion configuration
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/create_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ type CreateIssuesConfig struct {
GroupByDay *string `yaml:"group-by-day,omitempty"` // When true, if an open issue was already created today (UTC), post new content as a comment on it instead of creating a duplicate. Works best with close-older-issues: true.
Expires int `yaml:"expires,omitempty"` // Hours until the issue expires and should be automatically closed
Group *string `yaml:"group,omitempty"` // If true, group issues as sub-issues under a parent issue (workflow ID is used as group identifier)
Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept.
}

// parseCreateIssuesConfig handles create-issue configuration
Expand Down
84 changes: 42 additions & 42 deletions pkg/workflow/create_pull_request.go

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pkg/workflow/reply_to_pr_review_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type ReplyToPullRequestReviewCommentConfig struct {
BaseSafeOutputConfig `yaml:",inline"`
SafeOutputTargetConfig `yaml:",inline"`
SafeOutputFilterConfig `yaml:",inline"`
Footer *string `yaml:"footer,omitempty"` // Whether to add AI-generated footer to replies
}

// parseReplyToPullRequestReviewCommentConfig handles reply-to-pull-request-review-comment configuration
Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/safe_outputs_config_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1137,13 +1137,15 @@ func TestGenerateSafeOutputsConfigReplyToPullRequestReviewCommentWithTarget(t *t
data := &WorkflowData{
SafeOutputs: &SafeOutputsConfig{
ReplyToPullRequestReviewComment: &ReplyToPullRequestReviewCommentConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("10")},
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: strPtr("10"),
Footer: &footerTrue,
},
SafeOutputTargetConfig: SafeOutputTargetConfig{
Target: "pull_request",
TargetRepoSlug: "org/other-repo",
AllowedRepos: []string{"org/other-repo"},
},
Footer: &footerTrue,
},
},
}
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/safe_outputs_config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type BaseSafeOutputConfig struct {
GitHubToken string `yaml:"github-token,omitempty"` // GitHub token for this specific output type
GitHubApp *GitHubAppConfig `yaml:"github-app,omitempty"` // GitHub App credentials for minting a per-handler installation access token
Staged *TemplatableBool `yaml:"staged,omitempty"` // Templatable preview-only mode for this specific output type
Footer *string `yaml:"footer,omitempty"` // Controls AI-generated footer behavior. Values vary by handler; false omits the visible footer but keeps XML markers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] The shared Footer field silently accommodates two incompatible value vocabularies: most handlers treat it as a templatable bool ("true"/"false"), while submit_pr_review accepts "always"/"none"/"if-body". The comment "Values vary by handler" flags this but doesn't document the semantic divergence — future authors reading only the base struct will have no signal that some handlers reject the bool semantics.

💡 Suggestion

Tighten the comment to explicitly name the special case:

// Controls AI-generated footer behavior.
// Most handlers: templatable bool — false omits the visible footer but keeps XML markers.
// Exception — submit-pull-request-review: "always" (default), "none", or "if-body".
Footer *string `yaml:"footer,omitempty"`

This makes the divergence discoverable without needing to chase individual parsers.

@copilot please address this.

IssueIntent *bool `yaml:"issue-intent,omitempty"` // When true, enable issue-intent rationale/confidence guidance and schema requirements for this output type.
NormalizeClosingKeywords *bool `yaml:"normalize-closing-keywords,omitempty"` // When true for this output type, strip backticks from recognized issue-closing keywords in body fields.
// Samples carries deterministic replay samples for the hidden
Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/safe_outputs_footer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,10 @@ func TestFooterInHandlerConfig(t *testing.T) {
Name: "Test",
SafeOutputs: &SafeOutputsConfig{
CreateIssues: &CreateIssuesConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
Footer: strPtr("false"),
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: strPtr("1"),
Footer: strPtr("false"),
},
},
},
}
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/submit_pr_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ type SubmitPullRequestReviewConfig struct {
BaseSafeOutputConfig `yaml:",inline"`
SafeOutputTargetConfig `yaml:",inline"`
SafeOutputFilterConfig `yaml:",inline"`
Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text)
AllowedEvents []string `yaml:"allowed-events,omitempty"` // Optional list of allowed review event types: APPROVE, COMMENT, REQUEST_CHANGES. If omitted, all event types are allowed.
SupersedeOlderReviews bool `yaml:"supersede-older-reviews,omitempty"` // When true, dismisses older same-workflow REQUEST_CHANGES reviews after a replacement review is posted.
CommitId string `yaml:"commit-id,omitempty"` // When set, pins the review to this commit SHA instead of the current PR head.
Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/submit_pr_review_footer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,10 @@ func TestSubmitPRReviewFooterInHandlerConfig(t *testing.T) {
Name: "Test",
SafeOutputs: &SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
Footer: &footerValue,
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: strPtr("1"),
Footer: &footerValue,
},
},
CreatePullRequestReviewComments: &CreatePullRequestReviewCommentsConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("10")},
Expand Down
7 changes: 3 additions & 4 deletions pkg/workflow/update_discussion.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ var updateDiscussionLog = logger.New("workflow:update_discussion")
type UpdateDiscussionsConfig struct {
UpdateEntityConfig `yaml:",inline"`
SafeOutputAllowedLabelsConfig `yaml:",inline"`
Title *bool `yaml:"title,omitempty"` // Allow updating discussion title - presence indicates field can be updated
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
Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept.
Title *bool `yaml:"title,omitempty"` // Allow updating discussion title - presence indicates field can be updated
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
}

// parseUpdateDiscussionsConfig handles update-discussion configuration
Expand Down
29 changes: 22 additions & 7 deletions pkg/workflow/update_entity_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ type UpdateEntityParseOptions struct {
Logger *logger.Logger // Logger for this entity type
Fields []UpdateEntityFieldSpec // Field specifications to parse
CustomParser func(map[string]any) // Optional custom field parser
// AfterBaseParse is called with the parsed base config before entity-specific
// fields are parsed. This lets callers copy the base config into their own
// struct first, so field specs that point at promoted base fields (such as
// Footer) are not overwritten afterwards.
AfterBaseParse func(*UpdateEntityConfig)
}

// parseUpdateEntityConfigWithFields is a generic helper that reduces scaffolding duplication
Expand Down Expand Up @@ -356,6 +361,12 @@ func (c *Compiler) parseUpdateEntityConfigWithFields(
return nil, nil
}

// Let the caller copy the base config before entity-specific fields are parsed,
// so field specs writing to promoted base fields are not clobbered afterwards.
if opts.AfterBaseParse != nil {
opts.AfterBaseParse(baseConfig)
}

// Parse entity-specific bool fields according to specs
for _, field := range opts.Fields {
if field.Mode == FieldParsingTemplatableBool {
Expand Down Expand Up @@ -384,10 +395,10 @@ func (c *Compiler) parseUpdateEntityConfigWithFields(
// It handles the complete parsing flow:
// 1. Creates entity-specific config struct
// 2. Builds field specs with pointers to config fields
// 3. Calls parseUpdateEntityConfigWithFields
// 3. Calls parseUpdateEntityConfigWithFields, which copies the base config into
// the entity-specific struct before parsing entity-specific fields
// 4. Checks for nil result (early return)
// 5. Copies base config into entity-specific struct
// 6. Returns typed config
// 5. Returns typed config
//
// Type parameters:
// - T: The entity-specific config type (must embed UpdateEntityConfig)
Expand Down Expand Up @@ -443,6 +454,12 @@ func parseUpdateEntityConfigTyped[T any, PT interface {
ConfigKey: configKey,
Logger: logger,
Fields: fields,
// Assign the base config through the promoted setter on the embedded
// UpdateEntityConfig before entity-specific fields are parsed, so field
// specs targeting promoted base fields (e.g. Footer) survive.
AfterBaseParse: func(baseConfig *UpdateEntityConfig) {
PT(cfg).setUpdateEntityConfig(*baseConfig)
},
}

// Add custom parser wrapper if provided
Expand All @@ -452,14 +469,12 @@ func parseUpdateEntityConfigTyped[T any, PT interface {
}
}

// Parse base config and entity-specific fields
// Parse base config and entity-specific fields; the base config is assigned to
// cfg via AfterBaseParse before entity-specific fields are parsed.
baseConfig, _ := c.parseUpdateEntityConfigWithFields(outputMap, opts)
if baseConfig == nil {
return nil
}

// Assign the base config through the promoted setter on the embedded UpdateEntityConfig
PT(cfg).setUpdateEntityConfig(*baseConfig)

return cfg
}
1 change: 0 additions & 1 deletion pkg/workflow/update_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ 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 - boolean value controls permission (defaults to true)
Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept.
TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for issue validation - only issues with this prefix can be updated (deprecated: use required-title-prefix)
RequiredTitlePrefix string `yaml:"required-title-prefix,omitempty"` // Title prefix the issue must have (preferred over title-prefix)
RequiredLabels []string `yaml:"required-labels,omitempty"` // Labels that must ALL be present on the issue
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/update_pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ type UpdatePullRequestsConfig struct {
UpdateBranch *bool `yaml:"update-branch,omitempty"` // When true, update PR branch with latest base branch changes before applying other updates. Defaults to false.
UpdateBranchStacks *bool `yaml:"sync-stack,omitempty"` // When true, allow stacked-PR stack-sync fallback if update-branch endpoint is unsupported. Defaults to true.
Operation *string `yaml:"operation,omitempty"` // Default operation for body updates: "append", "prepend", or "replace" (defaults to "replace")
Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted.
}

// parseUpdatePullRequestsConfig handles update-pull-request configuration
Expand Down
1 change: 0 additions & 1 deletion pkg/workflow/update_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ var updateReleaseLog = logger.New("workflow:update_release")
// UpdateReleaseConfig holds configuration for updating GitHub releases from agent output
type UpdateReleaseConfig struct {
UpdateEntityConfig `yaml:",inline"`
Footer *string `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
Expand Down
Loading