From 60bc5ce9b9f9f2424546daa3a76da43205bbbee9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:27:58 +0000 Subject: [PATCH 1/3] Initial plan From 289173dd8137652faf36f635b95386e458ac2303 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:47:51 +0000 Subject: [PATCH 2/3] Split compiler_safe_outputs_config.go into builder, handlers, and orchestrator files Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9340cbbe-0b4f-4423-b4ec-47518c6c74da Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 123 +++ pkg/workflow/compiler_safe_outputs_config.go | 905 +----------------- .../compiler_safe_outputs_handlers.go | 783 +++++++++++++++ 3 files changed, 909 insertions(+), 902 deletions(-) create mode 100644 pkg/workflow/compiler_safe_outputs_builder.go create mode 100644 pkg/workflow/compiler_safe_outputs_handlers.go diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go new file mode 100644 index 00000000000..e21d57cb6cb --- /dev/null +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -0,0 +1,123 @@ +package workflow + +// handlerConfigBuilder provides a fluent API for building handler configurations +type handlerConfigBuilder struct { + config map[string]any +} + +// newHandlerConfigBuilder creates a new handler config builder +func newHandlerConfigBuilder() *handlerConfigBuilder { + return &handlerConfigBuilder{ + config: map[string]any{}, + } +} + +// AddIfPositive adds an integer field only if the value is greater than 0 +func (b *handlerConfigBuilder) AddIfPositive(key string, value int) *handlerConfigBuilder { + if value > 0 { + b.config[key] = value + } + return b +} + +// AddIfNotEmpty adds a string field only if the value is not empty +func (b *handlerConfigBuilder) AddIfNotEmpty(key string, value string) *handlerConfigBuilder { + if value != "" { + b.config[key] = value + } + return b +} + +// AddStringSlice adds a string slice field only if the slice is not empty +func (b *handlerConfigBuilder) AddStringSlice(key string, value []string) *handlerConfigBuilder { + if len(value) > 0 { + b.config[key] = value + } + return b +} + +// AddBoolPtr adds a boolean pointer field only if the pointer is not nil +func (b *handlerConfigBuilder) AddBoolPtr(key string, value *bool) *handlerConfigBuilder { + if value != nil { + b.config[key] = *value + } + return b +} + +// AddBoolPtrOrDefault adds a boolean field, using default if pointer is nil +func (b *handlerConfigBuilder) AddBoolPtrOrDefault(key string, value *bool, defaultValue bool) *handlerConfigBuilder { + if value != nil { + b.config[key] = *value + } else { + b.config[key] = defaultValue + } + return b +} + +// AddStringPtr adds a string pointer field only if the pointer is not nil +func (b *handlerConfigBuilder) AddStringPtr(key string, value *string) *handlerConfigBuilder { + if value != nil { + b.config[key] = *value + } + return b +} + +// AddDefault adds a field with a default value unconditionally +func (b *handlerConfigBuilder) AddDefault(key string, value any) *handlerConfigBuilder { + b.config[key] = value + return b +} + +// AddIfTrue adds a boolean field only if the value is true +func (b *handlerConfigBuilder) AddIfTrue(key string, value bool) *handlerConfigBuilder { + if value { + b.config[key] = true + } + return b +} + +// Build returns the built configuration map +func (b *handlerConfigBuilder) Build() map[string]any { + return b.config +} + +// handlerBuilder is a function that builds a handler config from SafeOutputsConfig +type handlerBuilder func(*SafeOutputsConfig) map[string]any + +// getEffectiveFooterForTemplatable returns the effective footer as a templatable string. +// If the local string footer is set, use it; otherwise convert the global bool footer. +// Returns nil if neither is set (default to true in JavaScript). +func getEffectiveFooterForTemplatable(localFooter *string, globalFooter *bool) *string { + if localFooter != nil { + return localFooter + } + if globalFooter != nil { + var s string + if *globalFooter { + s = "true" + } else { + s = "false" + } + return &s + } + return nil +} + +// getEffectiveFooterString returns the effective footer string value for a config. +// If the local string footer is set, use it; otherwise convert the global bool footer. +// Returns nil if neither is set (default to "always" in JavaScript). +func getEffectiveFooterString(localFooter *string, globalFooter *bool) *string { + if localFooter != nil { + return localFooter + } + if globalFooter != nil { + var s string + if *globalFooter { + s = "always" + } else { + s = "none" + } + return &s + } + return nil +} diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 12b747fba4a..e99b5c3237f 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -19,911 +19,12 @@ import ( // contracts. generateSafeOutputsConfig() in safe_outputs_config_generation.go // derives config.json from this same registry so both consumers stay in sync // without a separate generation path. +// +// Builder infrastructure (handlerConfigBuilder) lives in compiler_safe_outputs_builder.go. +// Handler registry entries live in compiler_safe_outputs_handlers.go. var compilerSafeOutputsConfigLog = logger.New("workflow:compiler_safe_outputs_config") -// getEffectiveFooterForTemplatable returns the effective footer as a templatable string. -// If the local string footer is set, use it; otherwise convert the global bool footer. -// Returns nil if neither is set (default to true in JavaScript). -func getEffectiveFooterForTemplatable(localFooter *string, globalFooter *bool) *string { - if localFooter != nil { - return localFooter - } - if globalFooter != nil { - var s string - if *globalFooter { - s = "true" - } else { - s = "false" - } - return &s - } - return nil -} - -// getEffectiveFooterString returns the effective footer string value for a config. -// If the local string footer is set, use it; otherwise convert the global bool footer. -// Returns nil if neither is set (default to "always" in JavaScript). -func getEffectiveFooterString(localFooter *string, globalFooter *bool) *string { - if localFooter != nil { - return localFooter - } - if globalFooter != nil { - var s string - if *globalFooter { - s = "always" - } else { - s = "none" - } - return &s - } - return nil -} - -// handlerConfigBuilder provides a fluent API for building handler configurations -type handlerConfigBuilder struct { - config map[string]any -} - -// newHandlerConfigBuilder creates a new handler config builder -func newHandlerConfigBuilder() *handlerConfigBuilder { - return &handlerConfigBuilder{ - config: map[string]any{}, - } -} - -// AddIfPositive adds an integer field only if the value is greater than 0 -func (b *handlerConfigBuilder) AddIfPositive(key string, value int) *handlerConfigBuilder { - if value > 0 { - b.config[key] = value - } - return b -} - -// AddIfNotEmpty adds a string field only if the value is not empty -func (b *handlerConfigBuilder) AddIfNotEmpty(key string, value string) *handlerConfigBuilder { - if value != "" { - b.config[key] = value - } - return b -} - -// AddStringSlice adds a string slice field only if the slice is not empty -func (b *handlerConfigBuilder) AddStringSlice(key string, value []string) *handlerConfigBuilder { - if len(value) > 0 { - b.config[key] = value - } - return b -} - -// AddBoolPtr adds a boolean pointer field only if the pointer is not nil -func (b *handlerConfigBuilder) AddBoolPtr(key string, value *bool) *handlerConfigBuilder { - if value != nil { - b.config[key] = *value - } - return b -} - -// AddBoolPtrOrDefault adds a boolean field, using default if pointer is nil -func (b *handlerConfigBuilder) AddBoolPtrOrDefault(key string, value *bool, defaultValue bool) *handlerConfigBuilder { - if value != nil { - b.config[key] = *value - } else { - b.config[key] = defaultValue - } - return b -} - -// AddStringPtr adds a string pointer field only if the pointer is not nil -func (b *handlerConfigBuilder) AddStringPtr(key string, value *string) *handlerConfigBuilder { - if value != nil { - b.config[key] = *value - } - return b -} - -// AddDefault adds a field with a default value unconditionally -func (b *handlerConfigBuilder) AddDefault(key string, value any) *handlerConfigBuilder { - b.config[key] = value - return b -} - -// AddIfTrue adds a boolean field only if the value is true -func (b *handlerConfigBuilder) AddIfTrue(key string, value bool) *handlerConfigBuilder { - if value { - b.config[key] = true - } - return b -} - -// Build returns the built configuration map -func (b *handlerConfigBuilder) Build() map[string]any { - return b.config -} - -// handlerBuilder is a function that builds a handler config from SafeOutputsConfig -type handlerBuilder func(*SafeOutputsConfig) map[string]any - -// handlerRegistry maps handler names to their builder functions -var handlerRegistry = map[string]handlerBuilder{ - "create_issue": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateIssues == nil { - return nil - } - c := cfg.CreateIssues - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed_labels", c.AllowedLabels). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfPositive("expires", c.Expires). - AddStringSlice("labels", c.Labels). - AddIfNotEmpty("title_prefix", c.TitlePrefix). - AddStringSlice("assignees", c.Assignees). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddTemplatableBool("group", c.Group). - AddTemplatableBool("close_older_issues", c.CloseOlderIssues). - AddIfNotEmpty("close_older_key", c.CloseOlderKey). - AddTemplatableBool("group_by_day", c.GroupByDay). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "add_comment": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AddComments == nil { - return nil - } - c := cfg.AddComments - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddTemplatableBool("hide_older_comments", c.HideOlderComments). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_discussion": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateDiscussions == nil { - return nil - } - c := cfg.CreateDiscussions - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("category", c.Category). - AddIfNotEmpty("title_prefix", c.TitlePrefix). - AddStringSlice("labels", c.Labels). - AddStringSlice("allowed_labels", c.AllowedLabels). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddTemplatableBool("close_older_discussions", c.CloseOlderDiscussions). - AddIfNotEmpty("close_older_key", c.CloseOlderKey). - AddIfNotEmpty("required_category", c.RequiredCategory). - AddIfPositive("expires", c.Expires). - AddBoolPtr("fallback_to_issue", c.FallbackToIssue). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "close_issue": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CloseIssues == nil { - return nil - } - c := cfg.CloseIssues - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddStringSlice("required_labels", c.RequiredLabels). - AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("state_reason", c.StateReason). - AddIfTrue("staged", c.Staged). - Build() - }, - "close_discussion": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CloseDiscussions == nil { - return nil - } - c := cfg.CloseDiscussions - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddStringSlice("required_labels", c.RequiredLabels). - AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfTrue("staged", c.Staged). - Build() - }, - "add_labels": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AddLabels == nil { - return nil - } - c := cfg.AddLabels - config := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddStringSlice("blocked", c.Blocked). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - // If config is empty, it means add_labels was explicitly configured with no options - // (null config), which means "allow any labels". Return non-nil empty map to - // indicate the handler is enabled. - if len(config) == 0 { - // Return empty map so handler is included in config - return make(map[string]any) - } - return config - }, - "remove_labels": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.RemoveLabels == nil { - return nil - } - c := cfg.RemoveLabels - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddStringSlice("blocked", c.Blocked). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "add_reviewer": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AddReviewer == nil { - return nil - } - c := cfg.AddReviewer - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Reviewers). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "assign_milestone": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AssignMilestone == nil { - return nil - } - c := cfg.AssignMilestone - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - AddIfTrue("auto_create", c.AutoCreate). - Build() - }, - "mark_pull_request_as_ready_for_review": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.MarkPullRequestAsReadyForReview == nil { - return nil - } - c := cfg.MarkPullRequestAsReadyForReview - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddStringSlice("required_labels", c.RequiredLabels). - AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateCodeScanningAlerts == nil { - return nil - } - c := cfg.CreateCodeScanningAlerts - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("driver", c.Driver). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_agent_session": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateAgentSessions == nil { - return nil - } - c := cfg.CreateAgentSessions - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("base", c.Base). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "update_issue": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UpdateIssues == nil { - return nil - } - c := cfg.UpdateIssues - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("title_prefix", c.TitlePrefix) - // Boolean pointer fields indicate which fields can be updated - if c.Status != nil { - builder.AddDefault("allow_status", true) - } - if c.Title != nil { - builder.AddDefault("allow_title", true) - } - // Body uses boolean value mode - add the actual boolean value - builder.AddBoolPtrOrDefault("allow_body", c.Body, true) - return builder. - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "update_discussion": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UpdateDiscussions == nil { - return nil - } - c := cfg.UpdateDiscussions - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target) - // Boolean pointer fields indicate which fields can be updated - if c.Title != nil { - builder.AddDefault("allow_title", true) - } - if c.Body != nil { - builder.AddDefault("allow_body", true) - } - if c.Labels != nil { - builder.AddDefault("allow_labels", true) - } - return builder. - AddStringSlice("allowed_labels", c.AllowedLabels). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "link_sub_issue": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.LinkSubIssue == nil { - return nil - } - c := cfg.LinkSubIssue - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("parent_required_labels", c.ParentRequiredLabels). - AddIfNotEmpty("parent_title_prefix", c.ParentTitlePrefix). - AddStringSlice("sub_required_labels", c.SubRequiredLabels). - AddIfNotEmpty("sub_title_prefix", c.SubTitlePrefix). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "update_release": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UpdateRelease == nil { - return nil - } - c := cfg.UpdateRelease - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreatePullRequestReviewComments == nil { - return nil - } - c := cfg.CreatePullRequestReviewComments - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("side", c.Side). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "submit_pull_request_review": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.SubmitPullRequestReview == nil { - return nil - } - c := cfg.SubmitPullRequestReview - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddStringSlice("allowed_events", c.AllowedEvents). - AddIfNotEmpty("github-token", c.GitHubToken). - AddStringPtr("footer", getEffectiveFooterString(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "reply_to_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.ReplyToPullRequestReviewComment == nil { - return nil - } - c := cfg.ReplyToPullRequestReviewComment - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfTrue("staged", c.Staged). - Build() - }, - "resolve_pull_request_review_thread": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.ResolvePullRequestReviewThread == nil { - return nil - } - c := cfg.ResolvePullRequestReviewThread - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_pull_request": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreatePullRequests == nil { - return nil - } - c := cfg.CreatePullRequests - maxPatchSize := 1024 // default 1024 KB - if cfg.MaximumPatchSize > 0 { - maxPatchSize = cfg.MaximumPatchSize - } - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("title_prefix", c.TitlePrefix). - AddStringSlice("labels", c.Labels). - AddStringSlice("reviewers", c.Reviewers). - AddStringSlice("assignees", c.Assignees). - AddTemplatableBool("draft", c.Draft). - AddIfNotEmpty("if_no_changes", c.IfNoChanges). - AddTemplatableBool("allow_empty", c.AllowEmpty). - AddTemplatableBool("auto_merge", c.AutoMerge). - AddIfPositive("expires", c.Expires). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddDefault("max_patch_size", maxPatchSize). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddBoolPtr("fallback_as_issue", c.FallbackAsIssue). - AddTemplatableBool("auto_close_issue", c.AutoCloseIssue). - AddIfNotEmpty("base_branch", c.BaseBranch). - AddStringPtr("protected_files_policy", c.ManifestFilesPolicy). - AddStringSlice("protected_files", getAllManifestFiles()). - AddStringSlice("protected_path_prefixes", getProtectedPathPrefixes()). - AddStringSlice("allowed_files", c.AllowedFiles). - AddStringSlice("excluded_files", c.ExcludedFiles). - AddIfTrue("preserve_branch_name", c.PreserveBranchName). - AddIfNotEmpty("patch_format", c.PatchFormat). - AddIfTrue("staged", c.Staged) - return builder.Build() - }, - "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.PushToPullRequestBranch == nil { - return nil - } - c := cfg.PushToPullRequestBranch - maxPatchSize := 1024 // default 1024 KB - if cfg.MaximumPatchSize > 0 { - maxPatchSize = cfg.MaximumPatchSize - } - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("title_prefix", c.TitlePrefix). - AddStringSlice("labels", c.Labels). - AddIfNotEmpty("if_no_changes", c.IfNoChanges). - AddIfNotEmpty("commit_title_suffix", c.CommitTitleSuffix). - AddDefault("max_patch_size", maxPatchSize). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - AddStringPtr("protected_files_policy", c.ManifestFilesPolicy). - AddStringSlice("protected_files", getAllManifestFiles()). - AddStringSlice("protected_path_prefixes", getProtectedPathPrefixes()). - AddStringSlice("allowed_files", c.AllowedFiles). - AddStringSlice("excluded_files", c.ExcludedFiles). - AddIfNotEmpty("patch_format", c.PatchFormat). - Build() - }, - "update_pull_request": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UpdatePullRequests == nil { - return nil - } - c := cfg.UpdatePullRequests - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddBoolPtrOrDefault("allow_title", c.Title, true). - AddBoolPtrOrDefault("allow_body", c.Body, true). - AddStringPtr("default_operation", c.Operation). - AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "close_pull_request": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.ClosePullRequests == nil { - return nil - } - c := cfg.ClosePullRequests - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target", c.Target). - AddStringSlice("required_labels", c.RequiredLabels). - AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "hide_comment": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.HideComment == nil { - return nil - } - c := cfg.HideComment - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed_reasons", c.AllowedReasons). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "dispatch_workflow": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.DispatchWorkflow == nil { - return nil - } - c := cfg.DispatchWorkflow - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("workflows", c.Workflows). - AddIfNotEmpty("target-repo", c.TargetRepoSlug) - - // Add workflow_files map if it has entries - if len(c.WorkflowFiles) > 0 { - builder.AddDefault("workflow_files", c.WorkflowFiles) - } - - // Add aw_context_workflows list if it has entries - if len(c.AwContextWorkflows) > 0 { - builder.AddStringSlice("aw_context_workflows", c.AwContextWorkflows) - } - - builder.AddIfNotEmpty("target-ref", c.TargetRef) - builder.AddIfNotEmpty("github-token", c.GitHubToken) - builder.AddIfTrue("staged", c.Staged) - return builder.Build() - }, - "dispatch_repository": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.DispatchRepository == nil || len(cfg.DispatchRepository.Tools) == 0 { - return nil - } - // Serialize each tool as a sub-map - tools := make(map[string]any, len(cfg.DispatchRepository.Tools)) - for toolKey, tool := range cfg.DispatchRepository.Tools { - toolConfig := newHandlerConfigBuilder(). - AddIfNotEmpty("workflow", tool.Workflow). - AddIfNotEmpty("event_type", tool.EventType). - AddIfNotEmpty("repository", tool.Repository). - AddStringSlice("allowed_repositories", tool.AllowedRepositories). - AddTemplatableInt("max", tool.Max). - AddIfNotEmpty("github-token", tool.GitHubToken). - AddIfTrue("staged", tool.Staged). - Build() - tools[toolKey] = toolConfig - } - return map[string]any{"tools": tools} - }, - "call_workflow": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CallWorkflow == nil { - return nil - } - c := cfg.CallWorkflow - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("workflows", c.Workflows) - - // Add workflow_files map if it has entries - if len(c.WorkflowFiles) > 0 { - builder.AddDefault("workflow_files", c.WorkflowFiles) - } - - builder.AddIfTrue("staged", c.Staged) - return builder.Build() - }, - "missing_tool": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.MissingTool == nil { - return nil - } - c := cfg.MissingTool - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "missing_data": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.MissingData == nil { - return nil - } - c := cfg.MissingData - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "noop": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.NoOp == nil { - return nil - } - c := cfg.NoOp - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringPtr("report-as-issue", c.ReportAsIssue). - AddIfTrue("staged", c.Staged). - Build() - }, - "report_incomplete": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.ReportIncomplete == nil { - return nil - } - c := cfg.ReportIncomplete - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_report_incomplete_issue": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.ReportIncomplete == nil { - return nil - } - c := cfg.ReportIncomplete - if !c.CreateIssue { - return nil - } - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("title-prefix", c.TitlePrefix). - AddStringSlice("labels", c.Labels). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "assign_to_agent": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AssignToAgent == nil { - return nil - } - c := cfg.AssignToAgent - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("name", c.DefaultAgent). - AddIfNotEmpty("model", c.DefaultModel). - AddIfNotEmpty("custom-agent", c.DefaultCustomAgent). - AddIfNotEmpty("custom-instructions", c.DefaultCustomInstructions). - AddStringSlice("allowed", c.Allowed). - AddIfTrue("ignore-if-error", c.IgnoreIfError). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed-repos", c.AllowedRepos). - AddIfNotEmpty("pull-request-repo", c.PullRequestRepoSlug). - AddStringSlice("allowed-pull-request-repos", c.AllowedPullRequestRepos). - AddIfNotEmpty("base-branch", c.BaseBranch). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "upload_asset": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UploadAssets == nil { - return nil - } - c := cfg.UploadAssets - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("branch", c.BranchName). - AddIfPositive("max-size", c.MaxSizeKB). - AddStringSlice("allowed-exts", c.AllowedExts). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "upload_artifact": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UploadArtifact == nil { - return nil - } - c := cfg.UploadArtifact - b := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfPositive("max-uploads", c.MaxUploads). - AddTemplatableInt("retention-days", c.RetentionDays). - AddTemplatableBool("skip-archive", c.SkipArchive). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged) - if c.MaxSizeBytes > 0 { - b = b.AddDefault("max-size-bytes", c.MaxSizeBytes) - } - if len(c.AllowedPaths) > 0 { - b = b.AddStringSlice("allowed-paths", c.AllowedPaths) - } - if c.Defaults != nil { - if c.Defaults.IfNoFiles != "" { - b = b.AddIfNotEmpty("default-if-no-files", c.Defaults.IfNoFiles) - } - } - if c.Filters != nil { - if len(c.Filters.Include) > 0 { - b = b.AddStringSlice("filters-include", c.Filters.Include) - } - if len(c.Filters.Exclude) > 0 { - b = b.AddStringSlice("filters-exclude", c.Filters.Exclude) - } - } - return b.Build() - }, - "autofix_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AutofixCodeScanningAlert == nil { - return nil - } - c := cfg.AutofixCodeScanningAlert - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - // Note: create_project, update_project and create_project_status_update are handled by the unified handler, - // not the separate project handler manager, so they are included in this registry. - "create_project": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateProjects == nil { - return nil - } - c := cfg.CreateProjects - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("target_owner", c.TargetOwner). - AddIfNotEmpty("title_prefix", c.TitlePrefix). - AddIfNotEmpty("github-token", c.GitHubToken) - if len(c.Views) > 0 { - builder.AddDefault("views", c.Views) - } - if len(c.FieldDefinitions) > 0 { - builder.AddDefault("field_definitions", c.FieldDefinitions) - } - builder.AddIfTrue("staged", c.Staged) - return builder.Build() - }, - "update_project": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UpdateProjects == nil { - return nil - } - c := cfg.UpdateProjects - builder := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfNotEmpty("project", c.Project). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos) - if len(c.Views) > 0 { - builder.AddDefault("views", c.Views) - } - if len(c.FieldDefinitions) > 0 { - builder.AddDefault("field_definitions", c.FieldDefinitions) - } - builder.AddIfTrue("staged", c.Staged) - return builder.Build() - }, - "assign_to_user": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.AssignToUser == nil { - return nil - } - c := cfg.AssignToUser - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddStringSlice("blocked", c.Blocked). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddTemplatableBool("unassign_first", c.UnassignFirst). - AddIfTrue("staged", c.Staged). - Build() - }, - "unassign_from_user": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.UnassignFromUser == nil { - return nil - } - c := cfg.UnassignFromUser - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddStringSlice("blocked", c.Blocked). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - }, - "create_project_status_update": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.CreateProjectStatusUpdates == nil { - return nil - } - c := cfg.CreateProjectStatusUpdates - return newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfNotEmpty("project", c.Project). - AddIfTrue("staged", c.Staged). - Build() - }, - "set_issue_type": func(cfg *SafeOutputsConfig) map[string]any { - if cfg.SetIssueType == nil { - return nil - } - c := cfg.SetIssueType - config := newHandlerConfigBuilder(). - AddTemplatableInt("max", c.Max). - AddStringSlice("allowed", c.Allowed). - AddIfNotEmpty("target", c.Target). - AddIfNotEmpty("target-repo", c.TargetRepoSlug). - AddStringSlice("allowed_repos", c.AllowedRepos). - AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() - // If config is empty, it means set_issue_type was explicitly configured with no options - // (null config), which means "allow any type". Return non-nil empty map to - // indicate the handler is enabled. - if len(config) == 0 { - return make(map[string]any) - } - return config - }, -} - func (c *Compiler) addHandlerManagerConfigEnvVar(steps *[]string, data *WorkflowData) { if data.SafeOutputs == nil { compilerSafeOutputsConfigLog.Print("No safe-outputs configuration, skipping handler manager config") diff --git a/pkg/workflow/compiler_safe_outputs_handlers.go b/pkg/workflow/compiler_safe_outputs_handlers.go new file mode 100644 index 00000000000..56f9f88fe50 --- /dev/null +++ b/pkg/workflow/compiler_safe_outputs_handlers.go @@ -0,0 +1,783 @@ +package workflow + +// handlerRegistry maps handler names to their builder functions. +// Each entry is keyed by the handler name used in GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG +// and returns a config map (nil means the handler is disabled). +var handlerRegistry = map[string]handlerBuilder{ + "create_issue": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateIssues == nil { + return nil + } + c := cfg.CreateIssues + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed_labels", c.AllowedLabels). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfPositive("expires", c.Expires). + AddStringSlice("labels", c.Labels). + AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddStringSlice("assignees", c.Assignees). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddTemplatableBool("group", c.Group). + AddTemplatableBool("close_older_issues", c.CloseOlderIssues). + AddIfNotEmpty("close_older_key", c.CloseOlderKey). + AddTemplatableBool("group_by_day", c.GroupByDay). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "add_comment": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AddComments == nil { + return nil + } + c := cfg.AddComments + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddTemplatableBool("hide_older_comments", c.HideOlderComments). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_discussion": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateDiscussions == nil { + return nil + } + c := cfg.CreateDiscussions + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("category", c.Category). + AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddStringSlice("labels", c.Labels). + AddStringSlice("allowed_labels", c.AllowedLabels). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddTemplatableBool("close_older_discussions", c.CloseOlderDiscussions). + AddIfNotEmpty("close_older_key", c.CloseOlderKey). + AddIfNotEmpty("required_category", c.RequiredCategory). + AddIfPositive("expires", c.Expires). + AddBoolPtr("fallback_to_issue", c.FallbackToIssue). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "close_issue": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CloseIssues == nil { + return nil + } + c := cfg.CloseIssues + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("state_reason", c.StateReason). + AddIfTrue("staged", c.Staged). + Build() + }, + "close_discussion": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CloseDiscussions == nil { + return nil + } + c := cfg.CloseDiscussions + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfTrue("staged", c.Staged). + Build() + }, + "add_labels": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AddLabels == nil { + return nil + } + c := cfg.AddLabels + config := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + // If config is empty, it means add_labels was explicitly configured with no options + // (null config), which means "allow any labels". Return non-nil empty map to + // indicate the handler is enabled. + if len(config) == 0 { + // Return empty map so handler is included in config + return make(map[string]any) + } + return config + }, + "remove_labels": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.RemoveLabels == nil { + return nil + } + c := cfg.RemoveLabels + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "add_reviewer": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AddReviewer == nil { + return nil + } + c := cfg.AddReviewer + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Reviewers). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "assign_milestone": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AssignMilestone == nil { + return nil + } + c := cfg.AssignMilestone + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + AddIfTrue("auto_create", c.AutoCreate). + Build() + }, + "mark_pull_request_as_ready_for_review": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.MarkPullRequestAsReadyForReview == nil { + return nil + } + c := cfg.MarkPullRequestAsReadyForReview + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateCodeScanningAlerts == nil { + return nil + } + c := cfg.CreateCodeScanningAlerts + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("driver", c.Driver). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_agent_session": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateAgentSessions == nil { + return nil + } + c := cfg.CreateAgentSessions + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("base", c.Base). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "update_issue": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UpdateIssues == nil { + return nil + } + c := cfg.UpdateIssues + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("title_prefix", c.TitlePrefix) + // Boolean pointer fields indicate which fields can be updated + if c.Status != nil { + builder.AddDefault("allow_status", true) + } + if c.Title != nil { + builder.AddDefault("allow_title", true) + } + // Body uses boolean value mode - add the actual boolean value + builder.AddBoolPtrOrDefault("allow_body", c.Body, true) + return builder. + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "update_discussion": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UpdateDiscussions == nil { + return nil + } + c := cfg.UpdateDiscussions + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target) + // Boolean pointer fields indicate which fields can be updated + if c.Title != nil { + builder.AddDefault("allow_title", true) + } + if c.Body != nil { + builder.AddDefault("allow_body", true) + } + if c.Labels != nil { + builder.AddDefault("allow_labels", true) + } + return builder. + AddStringSlice("allowed_labels", c.AllowedLabels). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "link_sub_issue": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.LinkSubIssue == nil { + return nil + } + c := cfg.LinkSubIssue + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("parent_required_labels", c.ParentRequiredLabels). + AddIfNotEmpty("parent_title_prefix", c.ParentTitlePrefix). + AddStringSlice("sub_required_labels", c.SubRequiredLabels). + AddIfNotEmpty("sub_title_prefix", c.SubTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "update_release": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UpdateRelease == nil { + return nil + } + c := cfg.UpdateRelease + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreatePullRequestReviewComments == nil { + return nil + } + c := cfg.CreatePullRequestReviewComments + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("side", c.Side). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "submit_pull_request_review": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.SubmitPullRequestReview == nil { + return nil + } + c := cfg.SubmitPullRequestReview + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddStringSlice("allowed_events", c.AllowedEvents). + AddIfNotEmpty("github-token", c.GitHubToken). + AddStringPtr("footer", getEffectiveFooterString(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "reply_to_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ReplyToPullRequestReviewComment == nil { + return nil + } + c := cfg.ReplyToPullRequestReviewComment + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). + Build() + }, + "resolve_pull_request_review_thread": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ResolvePullRequestReviewThread == nil { + return nil + } + c := cfg.ResolvePullRequestReviewThread + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_pull_request": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreatePullRequests == nil { + return nil + } + c := cfg.CreatePullRequests + maxPatchSize := 1024 // default 1024 KB + if cfg.MaximumPatchSize > 0 { + maxPatchSize = cfg.MaximumPatchSize + } + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddStringSlice("labels", c.Labels). + AddStringSlice("reviewers", c.Reviewers). + AddStringSlice("assignees", c.Assignees). + AddTemplatableBool("draft", c.Draft). + AddIfNotEmpty("if_no_changes", c.IfNoChanges). + AddTemplatableBool("allow_empty", c.AllowEmpty). + AddTemplatableBool("auto_merge", c.AutoMerge). + AddIfPositive("expires", c.Expires). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddDefault("max_patch_size", maxPatchSize). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddBoolPtr("fallback_as_issue", c.FallbackAsIssue). + AddTemplatableBool("auto_close_issue", c.AutoCloseIssue). + AddIfNotEmpty("base_branch", c.BaseBranch). + AddStringPtr("protected_files_policy", c.ManifestFilesPolicy). + AddStringSlice("protected_files", getAllManifestFiles()). + AddStringSlice("protected_path_prefixes", getProtectedPathPrefixes()). + AddStringSlice("allowed_files", c.AllowedFiles). + AddStringSlice("excluded_files", c.ExcludedFiles). + AddIfTrue("preserve_branch_name", c.PreserveBranchName). + AddIfNotEmpty("patch_format", c.PatchFormat). + AddIfTrue("staged", c.Staged) + return builder.Build() + }, + "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.PushToPullRequestBranch == nil { + return nil + } + c := cfg.PushToPullRequestBranch + maxPatchSize := 1024 // default 1024 KB + if cfg.MaximumPatchSize > 0 { + maxPatchSize = cfg.MaximumPatchSize + } + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddStringSlice("labels", c.Labels). + AddIfNotEmpty("if_no_changes", c.IfNoChanges). + AddIfNotEmpty("commit_title_suffix", c.CommitTitleSuffix). + AddDefault("max_patch_size", maxPatchSize). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + AddStringPtr("protected_files_policy", c.ManifestFilesPolicy). + AddStringSlice("protected_files", getAllManifestFiles()). + AddStringSlice("protected_path_prefixes", getProtectedPathPrefixes()). + AddStringSlice("allowed_files", c.AllowedFiles). + AddStringSlice("excluded_files", c.ExcludedFiles). + AddIfNotEmpty("patch_format", c.PatchFormat). + Build() + }, + "update_pull_request": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UpdatePullRequests == nil { + return nil + } + c := cfg.UpdatePullRequests + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddBoolPtrOrDefault("allow_title", c.Title, true). + AddBoolPtrOrDefault("allow_body", c.Body, true). + AddStringPtr("default_operation", c.Operation). + AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "close_pull_request": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ClosePullRequests == nil { + return nil + } + c := cfg.ClosePullRequests + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "hide_comment": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.HideComment == nil { + return nil + } + c := cfg.HideComment + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed_reasons", c.AllowedReasons). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "dispatch_workflow": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.DispatchWorkflow == nil { + return nil + } + c := cfg.DispatchWorkflow + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("workflows", c.Workflows). + AddIfNotEmpty("target-repo", c.TargetRepoSlug) + + // Add workflow_files map if it has entries + if len(c.WorkflowFiles) > 0 { + builder.AddDefault("workflow_files", c.WorkflowFiles) + } + + // Add aw_context_workflows list if it has entries + if len(c.AwContextWorkflows) > 0 { + builder.AddStringSlice("aw_context_workflows", c.AwContextWorkflows) + } + + builder.AddIfNotEmpty("target-ref", c.TargetRef) + builder.AddIfNotEmpty("github-token", c.GitHubToken) + builder.AddIfTrue("staged", c.Staged) + return builder.Build() + }, + "dispatch_repository": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.DispatchRepository == nil || len(cfg.DispatchRepository.Tools) == 0 { + return nil + } + // Serialize each tool as a sub-map + tools := make(map[string]any, len(cfg.DispatchRepository.Tools)) + for toolKey, tool := range cfg.DispatchRepository.Tools { + toolConfig := newHandlerConfigBuilder(). + AddIfNotEmpty("workflow", tool.Workflow). + AddIfNotEmpty("event_type", tool.EventType). + AddIfNotEmpty("repository", tool.Repository). + AddStringSlice("allowed_repositories", tool.AllowedRepositories). + AddTemplatableInt("max", tool.Max). + AddIfNotEmpty("github-token", tool.GitHubToken). + AddIfTrue("staged", tool.Staged). + Build() + tools[toolKey] = toolConfig + } + return map[string]any{"tools": tools} + }, + "call_workflow": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CallWorkflow == nil { + return nil + } + c := cfg.CallWorkflow + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("workflows", c.Workflows) + + // Add workflow_files map if it has entries + if len(c.WorkflowFiles) > 0 { + builder.AddDefault("workflow_files", c.WorkflowFiles) + } + + builder.AddIfTrue("staged", c.Staged) + return builder.Build() + }, + "missing_tool": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.MissingTool == nil { + return nil + } + c := cfg.MissingTool + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "missing_data": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.MissingData == nil { + return nil + } + c := cfg.MissingData + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "noop": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.NoOp == nil { + return nil + } + c := cfg.NoOp + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringPtr("report-as-issue", c.ReportAsIssue). + AddIfTrue("staged", c.Staged). + Build() + }, + "report_incomplete": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ReportIncomplete == nil { + return nil + } + c := cfg.ReportIncomplete + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_report_incomplete_issue": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ReportIncomplete == nil { + return nil + } + c := cfg.ReportIncomplete + if !c.CreateIssue { + return nil + } + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("title-prefix", c.TitlePrefix). + AddStringSlice("labels", c.Labels). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "assign_to_agent": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AssignToAgent == nil { + return nil + } + c := cfg.AssignToAgent + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("name", c.DefaultAgent). + AddIfNotEmpty("model", c.DefaultModel). + AddIfNotEmpty("custom-agent", c.DefaultCustomAgent). + AddIfNotEmpty("custom-instructions", c.DefaultCustomInstructions). + AddStringSlice("allowed", c.Allowed). + AddIfTrue("ignore-if-error", c.IgnoreIfError). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed-repos", c.AllowedRepos). + AddIfNotEmpty("pull-request-repo", c.PullRequestRepoSlug). + AddStringSlice("allowed-pull-request-repos", c.AllowedPullRequestRepos). + AddIfNotEmpty("base-branch", c.BaseBranch). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "upload_asset": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UploadAssets == nil { + return nil + } + c := cfg.UploadAssets + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("branch", c.BranchName). + AddIfPositive("max-size", c.MaxSizeKB). + AddStringSlice("allowed-exts", c.AllowedExts). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "upload_artifact": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UploadArtifact == nil { + return nil + } + c := cfg.UploadArtifact + b := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfPositive("max-uploads", c.MaxUploads). + AddTemplatableInt("retention-days", c.RetentionDays). + AddTemplatableBool("skip-archive", c.SkipArchive). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged) + if c.MaxSizeBytes > 0 { + b = b.AddDefault("max-size-bytes", c.MaxSizeBytes) + } + if len(c.AllowedPaths) > 0 { + b = b.AddStringSlice("allowed-paths", c.AllowedPaths) + } + if c.Defaults != nil { + if c.Defaults.IfNoFiles != "" { + b = b.AddIfNotEmpty("default-if-no-files", c.Defaults.IfNoFiles) + } + } + if c.Filters != nil { + if len(c.Filters.Include) > 0 { + b = b.AddStringSlice("filters-include", c.Filters.Include) + } + if len(c.Filters.Exclude) > 0 { + b = b.AddStringSlice("filters-exclude", c.Filters.Exclude) + } + } + return b.Build() + }, + "autofix_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AutofixCodeScanningAlert == nil { + return nil + } + c := cfg.AutofixCodeScanningAlert + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + // Note: create_project, update_project and create_project_status_update are handled by the unified handler, + // not the separate project handler manager, so they are included in this registry. + "create_project": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateProjects == nil { + return nil + } + c := cfg.CreateProjects + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target_owner", c.TargetOwner). + AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddIfNotEmpty("github-token", c.GitHubToken) + if len(c.Views) > 0 { + builder.AddDefault("views", c.Views) + } + if len(c.FieldDefinitions) > 0 { + builder.AddDefault("field_definitions", c.FieldDefinitions) + } + builder.AddIfTrue("staged", c.Staged) + return builder.Build() + }, + "update_project": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UpdateProjects == nil { + return nil + } + c := cfg.UpdateProjects + builder := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfNotEmpty("project", c.Project). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos) + if len(c.Views) > 0 { + builder.AddDefault("views", c.Views) + } + if len(c.FieldDefinitions) > 0 { + builder.AddDefault("field_definitions", c.FieldDefinitions) + } + builder.AddIfTrue("staged", c.Staged) + return builder.Build() + }, + "assign_to_user": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.AssignToUser == nil { + return nil + } + c := cfg.AssignToUser + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("unassign_first", c.UnassignFirst). + AddIfTrue("staged", c.Staged). + Build() + }, + "unassign_from_user": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.UnassignFromUser == nil { + return nil + } + c := cfg.UnassignFromUser + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + }, + "create_project_status_update": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.CreateProjectStatusUpdates == nil { + return nil + } + c := cfg.CreateProjectStatusUpdates + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfNotEmpty("project", c.Project). + AddIfTrue("staged", c.Staged). + Build() + }, + "set_issue_type": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.SetIssueType == nil { + return nil + } + c := cfg.SetIssueType + config := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed", c.Allowed). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + // If config is empty, it means set_issue_type was explicitly configured with no options + // (null config), which means "allow any type". Return non-nil empty map to + // indicate the handler is enabled. + if len(config) == 0 { + return make(map[string]any) + } + return config + }, +} From a2452e29c171e9b7a7869b50437d97f60b3cc9ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:56:34 +0000 Subject: [PATCH 3/3] docs(adr): add draft ADR-26297 for splitting compiler_safe_outputs_config.go by concern Generated by the Design Decision Gate workflow to document the architectural decision to split the 1,035-line compiler_safe_outputs_config.go into three concern-focused files: builder infrastructure, handler registry, and orchestrator. Co-Authored-By: Claude Sonnet 4.6 --- ...compiler-safe-outputs-config-by-concern.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/adr/26297-split-compiler-safe-outputs-config-by-concern.md diff --git a/docs/adr/26297-split-compiler-safe-outputs-config-by-concern.md b/docs/adr/26297-split-compiler-safe-outputs-config-by-concern.md new file mode 100644 index 00000000000..dc5f9310f5d --- /dev/null +++ b/docs/adr/26297-split-compiler-safe-outputs-config-by-concern.md @@ -0,0 +1,84 @@ +# ADR-26297: Split compiler_safe_outputs_config.go by Concern + +**Date**: 2026-04-14 +**Status**: Draft +**Deciders**: pelikhan, Copilot + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +`pkg/workflow/compiler_safe_outputs_config.go` had grown to 1,035 lines mixing three distinct responsibilities into a single file: (1) the fluent `handlerConfigBuilder` infrastructure and footer helper functions, (2) the `handlerRegistry` map containing 30+ individual handler configuration generator functions, and (3) the top-level orchestrator (`addHandlerManagerConfigEnvVar`) that drives the `GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG` env var generation. This made the file difficult to navigate—a contributor modifying one handler had to scroll through unrelated builder code and orchestration logic to find it. The file was a classic "God file" within an otherwise well-structured package. + +### Decision + +We will split `compiler_safe_outputs_config.go` into three files within the same `workflow` package, each owning a single concern: `compiler_safe_outputs_builder.go` holds the `handlerConfigBuilder` struct with its fluent methods, the `handlerBuilder` type alias, and footer helper functions; `compiler_safe_outputs_handlers.go` holds the `handlerRegistry` map with all individual handler config generator functions; and `compiler_safe_outputs_config.go` is reduced to 136 lines containing only the top-level orchestrator, workflow-call relay helpers, and engine agent file info. All files remain in `package workflow`, so no import paths or public APIs change. No logic is modified; this is a pure structural reorganization to improve file-level navigability. + +### Alternatives Considered + +#### Alternative 1: Keep Everything in a Single File + +The file could remain as-is. This is the simplest option—no merge conflicts, no navigability changes. It was rejected because 1,035 lines with three independent concerns (builder infrastructure, handler registry, orchestration) makes the file genuinely hard to navigate; finding any single handler requires scrolling through or searching within hundreds of lines of unrelated code. + +#### Alternative 2: Extract into a Separate Sub-Package + +The handler registry and builder could have been moved into a dedicated sub-package (e.g., `pkg/workflow/safeoutputsconfig/`). This would provide stronger compile-time boundaries and make the separation visible at the import level. It was not chosen because the handler functions reference unexported types in the `workflow` package, and moving them would require either exporting those types or significantly restructuring package boundaries—a change well beyond the scope of the navigability problem being solved. + +#### Alternative 3: Split by Type vs. Function Rather Than by Concern + +An alternative structure would group all struct/type definitions in one file and all functions in another, regardless of concern. This was rejected because it does not improve navigability for the primary use case: understanding or modifying all logic related to one concern (e.g., the handler registry). Concern-based grouping collocates related types and functions. + +### Consequences + +#### Positive +- Each new file is ≤800 lines and scoped to a single concern, improving navigability for contributors editing builder infrastructure, handler configs, or orchestration independently. +- The `handlerRegistry` in `compiler_safe_outputs_handlers.go` is now the unambiguous single source of truth for all handler names and field contracts, making it easy to audit or extend handlers without encountering unrelated code. +- `compiler_safe_outputs_config.go` is reduced from 1,035 to 136 lines; the orchestration intent is now immediately visible without builder or handler noise. +- No API surface changes—callers outside the package are unaffected. + +#### Negative +- The codebase now has more files, which adds overhead when doing a first-time sweep of the package. +- Cross-concern relationships (e.g., the orchestrator in `compiler_safe_outputs_config.go` iterating over `handlerRegistry` from `compiler_safe_outputs_handlers.go`) are less immediately visible than when everything was co-located. + +#### Neutral +- Go's intra-package visibility means all unexported identifiers remain accessible across the split files; no visibility changes are needed. +- IDE tooling and `go build` are unaffected by intra-package file splits. +- The `handlerRegistry` map (~783 lines) remains the largest single unit and is a candidate for further decomposition if the number of handlers grows substantially. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### File Organization + +1. Implementations **MUST** keep all `compiler_safe_outputs_config*.go` files in the same Go package (`package workflow`). +2. Implementations **MUST NOT** introduce a new sub-package solely to house the split files; the existing `pkg/workflow` package boundary **SHALL** be maintained. +3. Implementations **SHOULD** keep each split file focused on a single concern; a file **SHOULD NOT** contain logic belonging to another concern's designated file. + +### Builder Infrastructure + +1. The `handlerConfigBuilder` struct, all its fluent methods (`AddIfPositive`, `AddIfNotEmpty`, `AddStringSlice`, `AddBoolPtr`, `AddBoolPtrOrDefault`, `AddStringPtr`, `AddDefault`, `AddIfTrue`, `Build`, and any future additions), the `handlerBuilder` type alias, and the `getEffectiveFooter*` helper functions **MUST** reside in `compiler_safe_outputs_builder.go`. +2. Implementations **MUST NOT** duplicate the `handlerConfigBuilder` definition in another file; it **SHALL** be defined in exactly one location. + +### Handler Registry + +1. The `handlerRegistry` map and all individual handler config generator functions **MUST** reside in `compiler_safe_outputs_handlers.go`. +2. The `handlerRegistry` **MUST** be the single source of truth for handler names and field contracts; handler name strings **MUST NOT** be defined outside this map. +3. New handler implementations **MUST** be added as entries in `handlerRegistry` in `compiler_safe_outputs_handlers.go` and **MUST NOT** be inlined into the orchestrator. + +### Orchestration + +1. The top-level orchestrator function `addHandlerManagerConfigEnvVar`, the workflow-call relay helpers (`safeOutputsWithDispatchTargetRepo`, `safeOutputsWithDispatchTargetRef`), and the engine file info helper (`getEngineAgentFileInfo`) **MUST** remain in `compiler_safe_outputs_config.go`. +2. The orchestrator **MUST NOT** contain inline handler config construction logic; it **SHALL** delegate to `handlerRegistry` for all per-handler configuration. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. The primary conformance checks are: (a) all split files are in `package workflow`, (b) no new sub-package is created, (c) builder infrastructure is collocated in `compiler_safe_outputs_builder.go`, (d) the handler registry and all handler functions are collocated in `compiler_safe_outputs_handlers.go`, and (e) orchestration logic remains in `compiler_safe_outputs_config.go`. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24424691653) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*