From 2835a7992db8697de4820dabe5371499b284c613 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:09:03 +0000 Subject: [PATCH 1/6] Initial plan From fea76830600520db8fe5dcc44bfe99ac94f1e5ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:18:23 +0000 Subject: [PATCH 2/6] Fix create-issue deduplicate-by-title emission Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compile_outputs_issue_test.go | 12 ++++++++++++ pkg/workflow/create_issue.go | 7 ++++--- pkg/workflow/safe_outputs_config.go | 9 ++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pkg/workflow/compile_outputs_issue_test.go b/pkg/workflow/compile_outputs_issue_test.go index 384904e0f87..b51c438151b 100644 --- a/pkg/workflow/compile_outputs_issue_test.go +++ b/pkg/workflow/compile_outputs_issue_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/github/gh-aw/pkg/testutil" + "github.com/github/gh-aw/pkg/typeutil" ) // assertTokenInProcessSafeOutputsEnv verifies that a given environment variable name @@ -149,6 +150,7 @@ engine: claude strict: false safe-outputs: create-issue: + deduplicate-by-title: 1 title-prefix: "[genai] " labels: [copilot, automation] --- @@ -197,6 +199,11 @@ This workflow tests the output configuration parsing. t.Errorf("Expected label '%s' at index %d, got '%s'", expectedLabel, i, workflowData.SafeOutputs.CreateIssues.Labels[i]) } } + + deduplicateByTitle, ok := typeutil.ParseIntValue(workflowData.SafeOutputs.CreateIssues.DeduplicateByTitle) + if !ok || deduplicateByTitle != 1 { + t.Errorf("Expected deduplicate-by-title to parse as 1, got %#v", workflowData.SafeOutputs.CreateIssues.DeduplicateByTitle) + } } func TestOutputConfigEmpty(t *testing.T) { @@ -344,6 +351,7 @@ engine: claude strict: false safe-outputs: create-issue: + deduplicate-by-title: 1 title-prefix: "[genai] " labels: [copilot] --- @@ -410,6 +418,10 @@ This workflow tests the create-issue job generation. t.Error("Expected copilot label in handler config") } + if !strings.Contains(lockContent, `\"deduplicate_by_title\":1`) { + t.Error("Expected deduplicate_by_title in handler config") + } + // Verify job dependencies if !strings.Contains(lockContent, "needs:") { t.Error("Expected safe_outputs job to depend on main job") diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 4f09c97b05d..77e1e069f04 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -13,9 +13,10 @@ type CreateIssuesConfig struct { BaseSafeOutputConfig `yaml:",inline"` TitlePrefix string `yaml:"title-prefix,omitempty"` Labels []string `yaml:"labels,omitempty"` - AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). - AllowedFields []string `yaml:"allowed-fields,omitempty"` // Optional list of allowed issue field names. If omitted or empty, any issue fields are allowed. Use ["*"] to explicitly allow all. - Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to + AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). + AllowedFields []string `yaml:"allowed-fields,omitempty"` // Optional list of allowed issue field names. If omitted or empty, any issue fields are allowed. Use ["*"] to explicitly allow all. + Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to + DeduplicateByTitle any `yaml:"deduplicate-by-title,omitempty"` TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository issues AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that issues can be created in CloseOlderIssues *string `yaml:"close-older-issues,omitempty"` // When true, close older issues with same title prefix or labels as "not planned" diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 8c205adec19..509def4421a 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -784,7 +784,7 @@ var handlerRegistry = map[string]handlerBuilder{ return nil } c := cfg.CreateIssues - return newHandlerConfigBuilder(). + builder := newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddStringSlice("allowed_labels", c.AllowedLabels). AddStringSlice("allowed_fields", c.AllowedFields). @@ -800,8 +800,11 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableBool("group_by_day", c.GroupByDay). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged). - Build() + AddIfTrue("staged", c.Staged) + if c.DeduplicateByTitle != nil { + builder.AddDefault("deduplicate_by_title", c.DeduplicateByTitle) + } + return builder.Build() }, "add_comment": func(cfg *SafeOutputsConfig) map[string]any { if cfg.AddComments == nil { From 0e2d424b11944d498a5e82f805480c0111fa6cc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:19:30 +0000 Subject: [PATCH 3/6] Tighten create-issue dedup config emission Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 14 +++++++++++ pkg/workflow/create_issue.go | 24 +++++++++---------- pkg/workflow/safe_outputs_config.go | 6 ++--- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index c2a5ac15981..d1f1abc8c8b 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -71,6 +71,20 @@ func (b *handlerConfigBuilder) AddBoolPtr(key string, value *bool) *handlerConfi return b } +// AddBoolOrInt adds a boolean-or-integer field when the value is set. +// This preserves explicit false/0 values, which differ from an omitted field. +func (b *handlerConfigBuilder) AddBoolOrInt(key string, value any) *handlerConfigBuilder { + switch v := value.(type) { + case nil: + return b + case bool, int, int64, uint64: + b.config[key] = v + case float64: + b.config[key] = int(v) + } + 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 { diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 77e1e069f04..0e9b9035158 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -13,18 +13,18 @@ type CreateIssuesConfig struct { BaseSafeOutputConfig `yaml:",inline"` TitlePrefix string `yaml:"title-prefix,omitempty"` Labels []string `yaml:"labels,omitempty"` - AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). - AllowedFields []string `yaml:"allowed-fields,omitempty"` // Optional list of allowed issue field names. If omitted or empty, any issue fields are allowed. Use ["*"] to explicitly allow all. - Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to - DeduplicateByTitle any `yaml:"deduplicate-by-title,omitempty"` - TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository issues - AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that issues can be created in - CloseOlderIssues *string `yaml:"close-older-issues,omitempty"` // When true, close older issues with same title prefix or labels as "not planned" - CloseOlderKey string `yaml:"close-older-key,omitempty"` // Optional explicit deduplication key for close-older matching. When set, uses gh-aw-close-key marker instead of workflow-id markers. - 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. + AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). + AllowedFields []string `yaml:"allowed-fields,omitempty"` // Optional list of allowed issue field names. If omitted or empty, any issue fields are allowed. Use ["*"] to explicitly allow all. + Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to + DeduplicateByTitle any `yaml:"deduplicate-by-title,omitempty"` // When set to true, false, or a non-negative integer, enables title-based issue deduplication (exact or fuzzy by edit distance). + TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository issues + AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that issues can be created in + CloseOlderIssues *string `yaml:"close-older-issues,omitempty"` // When true, close older issues with same title prefix or labels as "not planned" + CloseOlderKey string `yaml:"close-older-key,omitempty"` // Optional explicit deduplication key for close-older matching. When set, uses gh-aw-close-key marker instead of workflow-id markers. + 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 diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 509def4421a..7395051719c 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -800,10 +800,8 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableBool("group_by_day", c.GroupByDay). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", c.GitHubToken). - AddIfTrue("staged", c.Staged) - if c.DeduplicateByTitle != nil { - builder.AddDefault("deduplicate_by_title", c.DeduplicateByTitle) - } + AddIfTrue("staged", c.Staged). + AddBoolOrInt("deduplicate_by_title", c.DeduplicateByTitle) return builder.Build() }, "add_comment": func(cfg *SafeOutputsConfig) map[string]any { From 91ca478ce1d50a4d9470530552d04f59dc28988a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:20:26 +0000 Subject: [PATCH 4/6] Harden bool-or-int config builder Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index d1f1abc8c8b..9317de8964d 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -1,6 +1,10 @@ package workflow -import "github.com/github/gh-aw/pkg/logger" +import ( + "math" + + "github.com/github/gh-aw/pkg/logger" +) var safeOutputsBuilderLog = logger.New("workflow:safe_outputs_builder") @@ -80,7 +84,13 @@ func (b *handlerConfigBuilder) AddBoolOrInt(key string, value any) *handlerConfi case bool, int, int64, uint64: b.config[key] = v case float64: - b.config[key] = int(v) + if math.Trunc(v) == v { + b.config[key] = int(v) + return b + } + safeOutputsBuilderLog.Printf("Ignoring non-integer float for %s: %v", key, v) + default: + safeOutputsBuilderLog.Printf("Ignoring unsupported bool-or-int value for %s: %T", key, value) } return b } From f95fae4905ac38a06626ab2ebcd3dfabafe0b7dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:22:17 +0000 Subject: [PATCH 5/6] Handle numeric dedup config safely Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 16 ++++++++++------ pkg/workflow/create_issue.go | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index 9317de8964d..82c9ee3cbca 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -4,6 +4,7 @@ import ( "math" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/typeutil" ) var safeOutputsBuilderLog = logger.New("workflow:safe_outputs_builder") @@ -81,17 +82,20 @@ func (b *handlerConfigBuilder) AddBoolOrInt(key string, value any) *handlerConfi switch v := value.(type) { case nil: return b - case bool, int, int64, uint64: + case bool: b.config[key] = v + return b case float64: - if math.Trunc(v) == v { - b.config[key] = int(v) + if math.Trunc(v) != v { + safeOutputsBuilderLog.Printf("Ignoring non-integer float for %s: %v", key, v) return b } - safeOutputsBuilderLog.Printf("Ignoring non-integer float for %s: %v", key, v) - default: - safeOutputsBuilderLog.Printf("Ignoring unsupported bool-or-int value for %s: %T", key, value) } + if intValue, ok := typeutil.ParseIntValue(value); ok { + b.config[key] = intValue + return b + } + safeOutputsBuilderLog.Printf("Ignoring unsupported bool-or-int value for %s: %T", key, value) return b } diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 0e9b9035158..3e06c219d92 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -16,7 +16,7 @@ type CreateIssuesConfig struct { AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). AllowedFields []string `yaml:"allowed-fields,omitempty"` // Optional list of allowed issue field names. If omitted or empty, any issue fields are allowed. Use ["*"] to explicitly allow all. Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to - DeduplicateByTitle any `yaml:"deduplicate-by-title,omitempty"` // When set to true, false, or a non-negative integer, enables title-based issue deduplication (exact or fuzzy by edit distance). + DeduplicateByTitle any `yaml:"deduplicate-by-title,omitempty"` // When true or 0, deduplicate by exact title match. When set to a positive integer N, also allow fuzzy matches up to edit distance N. When false or omitted, disable title-based deduplication. TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository issues AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that issues can be created in CloseOlderIssues *string `yaml:"close-older-issues,omitempty"` // When true, close older issues with same title prefix or labels as "not planned" From 3977c32e1e18c5c8df8eb4cf07b662eaf1b18848 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:23:10 +0000 Subject: [PATCH 6/6] Clarify float-to-int dedup handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index 82c9ee3cbca..d85c7f5f3bc 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -90,6 +90,10 @@ func (b *handlerConfigBuilder) AddBoolOrInt(key string, value any) *handlerConfi safeOutputsBuilderLog.Printf("Ignoring non-integer float for %s: %v", key, v) return b } + if intValue, ok := typeutil.ParseIntValue(v); ok { + b.config[key] = intValue + return b + } } if intValue, ok := typeutil.ParseIntValue(value); ok { b.config[key] = intValue