From 2252d614e2a4c1e55426027665945e433d9d1387 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:47:41 +0000 Subject: [PATCH 1/8] Initial plan From beb334f010a7fa9c466e9d9e9e6cb6735f8b7ef7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:53:36 +0000 Subject: [PATCH 2/8] Plan: unify close-older config fields Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/weekly-network-domains-audit.lock.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weekly-network-domains-audit.lock.yml b/.github/workflows/weekly-network-domains-audit.lock.yml index a274b6de207..d27c362ba8a 100644 --- a/.github/workflows/weekly-network-domains-audit.lock.yml +++ b/.github/workflows/weekly-network-domains-audit.lock.yml @@ -1505,7 +1505,7 @@ jobs: mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.4 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.4 --rootless - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: @@ -1520,6 +1520,7 @@ jobs: id: detection_agentic_execution if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true + timeout-minutes: 20 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE From 487a6741e830c7f69fa617dc3cbe0d9764b53ccb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:59:27 +0000 Subject: [PATCH 3/8] Refactor create close-older config into shared embed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...piler_safe_outputs_config_handlers_test.go | 4 +- .../compiler_safe_outputs_config_test.go | 10 ++-- .../create_close_older_config_test.go | 55 +++++++++++++++++++ pkg/workflow/create_discussion.go | 32 ++++++----- pkg/workflow/create_entity_helpers.go | 15 +++++ pkg/workflow/create_issue.go | 14 ++--- pkg/workflow/create_pull_request.go | 10 ++-- pkg/workflow/safe_outputs_handler_registry.go | 12 ++-- 8 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 pkg/workflow/create_close_older_config_test.go diff --git a/pkg/workflow/compiler_safe_outputs_config_handlers_test.go b/pkg/workflow/compiler_safe_outputs_config_handlers_test.go index 963f057b1a2..ef040a062f0 100644 --- a/pkg/workflow/compiler_safe_outputs_config_handlers_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_handlers_test.go @@ -279,7 +279,9 @@ func TestHandlerConfigBooleanFields(t *testing.T) { name: "close older discussions", safeOutputs: &SafeOutputsConfig{ CreateDiscussions: &CreateDiscussionsConfig{ - CloseOlderDiscussions: strPtr("true"), + CloseOlderConfig: CloseOlderConfig{ + Enabled: strPtr("true"), + }, }, }, checkField: "create_discussion", diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index 7df314ca975..bb35ac9f5ff 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -64,10 +64,12 @@ func TestAddHandlerManagerConfigEnvVar(t *testing.T) { BaseSafeOutputConfig: BaseSafeOutputConfig{ Max: strPtr("2"), }, - Category: "general", - TitlePrefix: "[Discussion] ", - Labels: []string{"ai"}, - CloseOlderDiscussions: strPtr("true"), + Category: "general", + TitlePrefix: "[Discussion] ", + Labels: []string{"ai"}, + CloseOlderConfig: CloseOlderConfig{ + Enabled: strPtr("true"), + }, }, }, checkContains: []string{ diff --git a/pkg/workflow/create_close_older_config_test.go b/pkg/workflow/create_close_older_config_test.go new file mode 100644 index 00000000000..f1878333799 --- /dev/null +++ b/pkg/workflow/create_close_older_config_test.go @@ -0,0 +1,55 @@ +//go:build !integration + +package workflow + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseCreateIssuesConfigMapsCloseOlderConfig(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateIssuesConfig(map[string]any{ + "create-issue": map[string]any{ + "close-older-issues": true, + "close-older-key": "issue-key", + }, + }) + + require.NotNil(t, config) + require.NotNil(t, config.CloseOlderConfig.Enabled) + assert.Equal(t, "true", *config.CloseOlderConfig.Enabled) + assert.Equal(t, "issue-key", config.CloseOlderConfig.Key) +} + +func TestParseCreateDiscussionsConfigMapsCloseOlderConfig(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateDiscussionsConfig(map[string]any{ + "create-discussion": map[string]any{ + "close-older-discussions": "${{ true }}", + "close-older-key": "discussion-key", + }, + }) + + require.NotNil(t, config) + require.NotNil(t, config.CloseOlderConfig.Enabled) + assert.Equal(t, "${{ true }}", *config.CloseOlderConfig.Enabled) + assert.Equal(t, "discussion-key", config.CloseOlderConfig.Key) +} + +func TestParseCreatePullRequestsConfigMapsCloseOlderConfig(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreatePullRequestsConfig(map[string]any{ + "create-pull-request": map[string]any{ + "close-older-pull-requests": true, + "close-older-key": "pull-request-key", + }, + }) + + require.NotNil(t, config) + require.NotNil(t, config.CloseOlderConfig.Enabled) + assert.Equal(t, "true", *config.CloseOlderConfig.Enabled) + assert.Equal(t, "pull-request-key", config.CloseOlderConfig.Key) +} diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index b4d691a9833..4a4160ce872 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -14,18 +14,17 @@ var discussionLog = logger.New("workflow:create_discussion") type CreateDiscussionsConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputAllowedLabelsConfig `yaml:",inline"` - TitlePrefix string `yaml:"title-prefix,omitempty"` - Category string `yaml:"category,omitempty"` // Discussion category ID or name - MinBodyLength int `yaml:"min-body-length,omitempty"` // Minimum required discussion body length before footer/markers - Labels []string `yaml:"labels,omitempty"` // Labels to attach to discussions and match when closing older ones - TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository discussions - AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that discussions can be created in - CloseOlderDiscussions *string `yaml:"close-older-discussions,omitempty"` // When true, close older discussions with same title prefix or labels as outdated - 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. - 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. + TitlePrefix string `yaml:"title-prefix,omitempty"` + Category string `yaml:"category,omitempty"` // Discussion category ID or name + MinBodyLength int `yaml:"min-body-length,omitempty"` // Minimum required discussion body length before footer/markers + Labels []string `yaml:"labels,omitempty"` // Labels to attach to discussions and match when closing older ones + TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository discussions + AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that discussions can be created in + CloseOlderConfig `yaml:",inline"` // Shared close-older settings; Enabled is sourced from close-older-discussions. + 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 @@ -34,7 +33,7 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat outputMap, "create-discussion", CreateParseOptions{ - BoolFields: []string{"close-older-discussions", "footer"}, + BoolFields: []string{"close-older-discussions", "close-older-enabled", "footer"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -44,7 +43,10 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat // For backward compatibility, handle nil/empty config return &CreateDiscussionsConfig{} }, - nil, + func(configData map[string]any) bool { + setCloseOlderEnabledAlias(configData, "close-older-discussions") + return true + }, func(_ map[string]any, config *CreateDiscussionsConfig, expiresDisabled bool) { // Set default max if not specified if config.Max == nil { @@ -94,7 +96,7 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat if len(config.AllowedRepos) > 0 { discussionLog.Printf("Allowed repos configured: %v", config.AllowedRepos) } - if config.CloseOlderDiscussions != nil { + if config.CloseOlderConfig.Enabled != nil { discussionLog.Print("Close older discussions flag set") if config.RequiredCategory != "" { discussionLog.Printf("Required category for close older discussions: %q", config.RequiredCategory) diff --git a/pkg/workflow/create_entity_helpers.go b/pkg/workflow/create_entity_helpers.go index 04e6691c0b6..654cf05230a 100644 --- a/pkg/workflow/create_entity_helpers.go +++ b/pkg/workflow/create_entity_helpers.go @@ -13,6 +13,21 @@ type CreateParseOptions struct { HandleExpires bool } +// CloseOlderConfig holds shared close-older settings across create entity handlers. +type CloseOlderConfig struct { + Enabled *string `yaml:"close-older-enabled,omitempty"` // Internal canonical key; populated from entity-specific close-older-* keys before unmarshaling. + Key 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. +} + +func setCloseOlderEnabledAlias(configData map[string]any, sourceKey string) { + if configData == nil { + return + } + if value, exists := configData[sourceKey]; exists { + configData["close-older-enabled"] = value + } +} + // parseCreateEntityConfig parses create-* config scaffolding shared by issue/discussion/PR handlers. // // Parameters: diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 5b2622234f0..d045eb274b8 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -20,12 +20,11 @@ type CreateIssuesConfig struct { DeduplicateByTitle *TemplatableBoolOrInt `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. Accepts GitHub Actions expressions. 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. + CloseOlderConfig `yaml:",inline"` // Shared close-older settings; Enabled is sourced from close-older-issues. + 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 @@ -34,7 +33,7 @@ func (c *Compiler) parseCreateIssuesConfig(outputMap map[string]any) *CreateIssu outputMap, "create-issue", CreateParseOptions{ - BoolFields: []string{"close-older-issues", "group", "footer", "group-by-day"}, + BoolFields: []string{"close-older-issues", "close-older-enabled", "group", "footer", "group-by-day"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -45,6 +44,7 @@ func (c *Compiler) parseCreateIssuesConfig(outputMap map[string]any) *CreateIssu return &CreateIssuesConfig{} }, func(configData map[string]any) bool { + setCloseOlderEnabledAlias(configData, "close-older-issues") coerceStringOrArrayFields(configData, []string{"assignees"}, createIssueLog) return true }, diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index a8c561bd3af..35ef946fbd9 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -25,10 +25,10 @@ func getFallbackAsIssue(config *CreatePullRequestsConfig) bool { // value, including GitHub Actions expressions like "${{ ... }}", is treated as enabled. // Used for compile-time permission calculation. func isCloseOlderPullRequestsEnabled(config *CreatePullRequestsConfig) bool { - if config == nil || config.CloseOlderPullRequests == nil { + if config == nil || config.CloseOlderConfig.Enabled == nil { return false } - v := *config.CloseOlderPullRequests + v := *config.CloseOlderConfig.Enabled return v != "" && v != "false" && v != "0" } @@ -130,8 +130,7 @@ type CreatePullRequestsConfig struct { PatchFormat string `yaml:"patch-format,omitempty"` // Transport format for packaging changes: "bundle" (default, uses git bundle and preserves merge topology/per-commit metadata) or "am" (uses git format-patch). SignedCommits *bool `yaml:"signed-commits,omitempty"` // When false, skips GitHub GraphQL signed commits and pushes the local git history directly. Default is true. AllowWorkflows bool `yaml:"allow-workflows,omitempty"` // When true, adds workflows: write to the GitHub App token. Requires safe-outputs.github-app to be configured. - CloseOlderPullRequests *string `yaml:"close-older-pull-requests,omitempty"` // When true, close older open pull requests with the same workflow-id marker when a new one is created. Capped at 10 closures per run. - 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. + CloseOlderConfig `yaml:",inline"` // Shared close-older settings; Enabled is sourced from close-older-pull-requests. } // parseCreatePullRequestsConfig handles only create-pull-request (singular) configuration @@ -147,7 +146,7 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea outputMap, "create-pull-request", CreateParseOptions{ - BoolFields: []string{"draft", "allow-empty", "footer", "auto-close-issue", "close-older-pull-requests"}, + BoolFields: []string{"draft", "allow-empty", "footer", "auto-close-issue", "close-older-pull-requests", "close-older-enabled"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -158,6 +157,7 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea return &CreatePullRequestsConfig{} }, func(configData map[string]any) bool { + setCloseOlderEnabledAlias(configData, "close-older-pull-requests") coerceStringOrArrayFields(configData, createPRStringOrArrayFields, createPRLog) // Pre-process protected-files: supports string enum OR object form {policy, exclude}. diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index ae3b4305c12..341fecc4706 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -72,8 +72,8 @@ var handlerRegistry = map[string]handlerBuilder{ 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("close_older_issues", c.CloseOlderConfig.Enabled). + AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddTemplatableBool("group_by_day", c.GroupByDay). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", resolveHandlerGitHubToken(c.GitHubApp, "create-issue", c.GitHubToken)). @@ -117,8 +117,8 @@ var handlerRegistry = map[string]handlerBuilder{ 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). + AddTemplatableBool("close_older_discussions", c.CloseOlderConfig.Enabled). + AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddIfNotEmpty("required_category", c.RequiredCategory). AddIfPositive("expires", c.Expires). AddBoolPtr("fallback_to_issue", c.FallbackToIssue). @@ -588,8 +588,8 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfTrue("recreate_ref", c.RecreateRef). AddIfNotEmpty("patch_format", c.PatchFormat). AddBoolPtr("signed_commits", c.SignedCommits). - AddTemplatableBool("close_older_pull_requests", c.CloseOlderPullRequests). - AddIfNotEmpty("close_older_key", c.CloseOlderKey). + AddTemplatableBool("close_older_pull_requests", c.CloseOlderConfig.Enabled). + AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)) if c.PreCreate { builder. From 727498e496761eb125162983498a3fa60bf19e37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:38:30 +0000 Subject: [PATCH 4/8] docs(adr): add draft ADR-54656 for shared CloseOlderConfig embed --- ...ify-close-older-config-via-shared-embed.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/adr/54656-unify-close-older-config-via-shared-embed.md diff --git a/docs/adr/54656-unify-close-older-config-via-shared-embed.md b/docs/adr/54656-unify-close-older-config-via-shared-embed.md new file mode 100644 index 00000000000..5e6e00385cc --- /dev/null +++ b/docs/adr/54656-unify-close-older-config-via-shared-embed.md @@ -0,0 +1,50 @@ +# ADR-54656: Unify close-older Config Fields via Shared Struct Embed + +**Date**: 2026-08-22 +**Status**: Draft +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +`create-issue`, `create-discussion`, and `create-pull-request` each independently declared close-older enable (`CloseOlderIssues`, `CloseOlderDiscussions`, `CloseOlderPullRequests`) and key (`CloseOlderKey`) fields on their respective config structs. Downstream handler consumers in `safe_outputs_handler_registry.go` accessed these fields through parallel but structurally identical code paths. Any behavioural change to close-older logic required coordinated edits across three config struct definitions and three consumer call sites, with no compiler enforcement that they stayed in sync. + +### Decision + +We will introduce a shared `CloseOlderConfig` struct in `pkg/workflow/create_entity_helpers.go` containing canonical `Enabled *string` and `Key string` fields, and embed it inline into `CreateIssuesConfig`, `CreateDiscussionsConfig`, and `CreatePullRequestsConfig`. Existing public YAML keys (`close-older-issues`, `close-older-discussions`, `close-older-pull-requests`) are preserved through parse-time aliasing via `setCloseOlderEnabledAlias`, which copies the entity-specific value to the internal canonical key `close-older-enabled` before YAML unmarshaling. All downstream consumers are updated to read from `CloseOlderConfig.Enabled` and `CloseOlderConfig.Key`. + +### Alternatives Considered + +#### Alternative 1: Shared accessor functions without struct consolidation + +Keep per-entity fields on each config struct but introduce a shared interface or helper functions to access them uniformly. This would allow downstream code to call a common accessor rather than field-by-field paths, without changing the struct layout. + +**Why not chosen**: The struct duplication itself is the problem — the accessors would hide it but not eliminate it. New close-older options would still require changes in three places. The embed approach removes that duplication at the type level, giving the compiler the ability to catch missed updates. + +#### Alternative 2: Unified YAML key without backward-compatible aliasing + +Replace the three entity-specific YAML keys with a single `close-older-enabled` key across all handler configs, removing the aliasing step. + +**Why not chosen**: This would be a breaking change for all existing workflow YAML files that use `close-older-issues`, `close-older-discussions`, or `close-older-pull-requests` keys. The aliasing approach achieves consolidation internally while keeping the public API stable, which is required for backward compatibility. + +### Consequences + +#### Positive +- Single source of truth for close-older configuration: `CloseOlderConfig` is defined once and embedded by reference everywhere. +- Downstream consumers (`safe_outputs_handler_registry.go`) read from a uniform path (`c.CloseOlderConfig.Enabled`, `c.CloseOlderConfig.Key`) regardless of the originating handler type. +- New close-older fields only need to be added to `CloseOlderConfig` to propagate across all three create handlers automatically. +- Parser coverage for all three aliasing paths is validated by dedicated tests in `create_close_older_config_test.go`. + +#### Negative +- Parse-time aliasing introduces non-obvious indirection: the internal canonical YAML key (`close-older-enabled`) differs from the public YAML API keys, and the mapping happens via `setCloseOlderEnabledAlias` before unmarshaling — a reader unfamiliar with this pattern may be confused by `BoolFields` including both the public and canonical keys. +- The embed uses `yaml:",inline"`, which means YAML tags on `CloseOlderConfig` fields coexist in the same namespace as the parent struct's tags; tag conflicts in future fields require care. + +#### Neutral +- Existing per-entity YAML keys continue to work unchanged; no migration of consumer configs is required. +- The `close-older-key` YAML tag is shared across all three handlers through the embed, making its semantics consistent by construction. +- `isCloseOlderPullRequestsEnabled` is updated to dereference `config.CloseOlderConfig.Enabled` instead of the old `config.CloseOlderPullRequests` field. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From b1525ba3f1556400acb23592032961924890b56e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:38:53 +0000 Subject: [PATCH 5/8] Address review feedback: make CloseOlderConfig.Enabled internal-only, add absence/false-case tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- ...ify-close-older-config-via-shared-embed.md | 4 +- .../create_close_older_config_test.go | 62 +++++++++++++++++++ pkg/workflow/create_discussion.go | 11 ++-- pkg/workflow/create_entity_helpers.go | 26 ++++++-- pkg/workflow/create_issue.go | 7 ++- pkg/workflow/create_pull_request.go | 5 +- pkg/workflow/safe_outputs_handler_registry.go | 5 ++ 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/docs/adr/54656-unify-close-older-config-via-shared-embed.md b/docs/adr/54656-unify-close-older-config-via-shared-embed.md index 5e6e00385cc..3bdf1d3ebf7 100644 --- a/docs/adr/54656-unify-close-older-config-via-shared-embed.md +++ b/docs/adr/54656-unify-close-older-config-via-shared-embed.md @@ -12,7 +12,7 @@ ### Decision -We will introduce a shared `CloseOlderConfig` struct in `pkg/workflow/create_entity_helpers.go` containing canonical `Enabled *string` and `Key string` fields, and embed it inline into `CreateIssuesConfig`, `CreateDiscussionsConfig`, and `CreatePullRequestsConfig`. Existing public YAML keys (`close-older-issues`, `close-older-discussions`, `close-older-pull-requests`) are preserved through parse-time aliasing via `setCloseOlderEnabledAlias`, which copies the entity-specific value to the internal canonical key `close-older-enabled` before YAML unmarshaling. All downstream consumers are updated to read from `CloseOlderConfig.Enabled` and `CloseOlderConfig.Key`. +We will introduce a shared `CloseOlderConfig` struct in `pkg/workflow/create_entity_helpers.go` containing canonical `Enabled *string` and `Key string` fields, and embed it inline into `CreateIssuesConfig`, `CreateDiscussionsConfig`, and `CreatePullRequestsConfig`. Existing public YAML keys (`close-older-issues`, `close-older-discussions`, `close-older-pull-requests`) are preserved: `Enabled` is tagged `yaml:"-"` so it is never directly settable from workflow frontmatter, and is instead populated after unmarshaling via `closeOlderEnabledFromConfigData`, which reads the already-preprocessed entity-specific value out of the raw config map. All downstream consumers are updated to read from `CloseOlderConfig.Enabled` and `CloseOlderConfig.Key`. ### Alternatives Considered @@ -37,7 +37,7 @@ Replace the three entity-specific YAML keys with a single `close-older-enabled` - Parser coverage for all three aliasing paths is validated by dedicated tests in `create_close_older_config_test.go`. #### Negative -- Parse-time aliasing introduces non-obvious indirection: the internal canonical YAML key (`close-older-enabled`) differs from the public YAML API keys, and the mapping happens via `setCloseOlderEnabledAlias` before unmarshaling — a reader unfamiliar with this pattern may be confused by `BoolFields` including both the public and canonical keys. +- Post-unmarshal population introduces a small amount of indirection: `Enabled` is not set by YAML unmarshaling directly but by an explicit call to `closeOlderEnabledFromConfigData` in each handler's `postUnmarshal` callback, reading the already-preprocessed entity-specific key. A reader unfamiliar with this pattern may be confused that `CloseOlderConfig.Enabled` has no YAML tag despite being populated from YAML input. - The embed uses `yaml:",inline"`, which means YAML tags on `CloseOlderConfig` fields coexist in the same namespace as the parent struct's tags; tag conflicts in future fields require care. #### Neutral diff --git a/pkg/workflow/create_close_older_config_test.go b/pkg/workflow/create_close_older_config_test.go index f1878333799..cf75e2041af 100644 --- a/pkg/workflow/create_close_older_config_test.go +++ b/pkg/workflow/create_close_older_config_test.go @@ -53,3 +53,65 @@ func TestParseCreatePullRequestsConfigMapsCloseOlderConfig(t *testing.T) { assert.Equal(t, "true", *config.CloseOlderConfig.Enabled) assert.Equal(t, "pull-request-key", config.CloseOlderConfig.Key) } + +func TestParseCreateIssuesConfigNoCloseOlderWhenAbsent(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateIssuesConfig(map[string]any{ + "create-issue": map[string]any{}, + }) + + require.NotNil(t, config) + assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.CloseOlderConfig.Key) +} + +func TestParseCreateIssuesConfigCloseOlderExplicitFalse(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateIssuesConfig(map[string]any{ + "create-issue": map[string]any{ + "close-older-issues": false, + }, + }) + + require.NotNil(t, config) + require.NotNil(t, config.CloseOlderConfig.Enabled, "Enabled should be set (not nil) when explicitly false") + assert.Equal(t, "false", *config.CloseOlderConfig.Enabled) +} + +func TestParseCreateDiscussionsConfigNoCloseOlderWhenAbsent(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateDiscussionsConfig(map[string]any{ + "create-discussion": map[string]any{}, + }) + + require.NotNil(t, config) + assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.CloseOlderConfig.Key) +} + +func TestParseCreatePullRequestsConfigNoCloseOlderWhenAbsent(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreatePullRequestsConfig(map[string]any{ + "create-pull-request": map[string]any{}, + }) + + require.NotNil(t, config) + assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.CloseOlderConfig.Key) +} + +// TestParseCreateIssuesConfigCloseOlderEnabledIsNotAPublicKey verifies that the internal +// canonical field name (matching the shared CloseOlderConfig.Enabled tag prior to this +// change) is not accepted directly as a workflow-authored YAML key, since Enabled is now +// tagged yaml:"-" and only populated via closeOlderEnabledFromConfigData. +func TestParseCreateIssuesConfigCloseOlderEnabledIsNotAPublicKey(t *testing.T) { + compiler := NewCompiler(WithFailFast(true)) + config := compiler.parseCreateIssuesConfig(map[string]any{ + "create-issue": map[string]any{ + "close-older-enabled": true, + }, + }) + + require.NotNil(t, config) + assert.Nil(t, config.CloseOlderConfig.Enabled, "close-older-enabled must not be a supported public YAML key") +} diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index 4a4160ce872..a1ccbdda5e4 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -33,7 +33,7 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat outputMap, "create-discussion", CreateParseOptions{ - BoolFields: []string{"close-older-discussions", "close-older-enabled", "footer"}, + BoolFields: []string{"close-older-discussions", "footer"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -43,11 +43,10 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat // For backward compatibility, handle nil/empty config return &CreateDiscussionsConfig{} }, - func(configData map[string]any) bool { - setCloseOlderEnabledAlias(configData, "close-older-discussions") - return true - }, - func(_ map[string]any, config *CreateDiscussionsConfig, expiresDisabled bool) { + nil, + func(configData map[string]any, config *CreateDiscussionsConfig, expiresDisabled bool) { + config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-discussions") + // Set default max if not specified if config.Max == nil { config.Max = defaultIntStr(1) diff --git a/pkg/workflow/create_entity_helpers.go b/pkg/workflow/create_entity_helpers.go index 654cf05230a..38bc39194b5 100644 --- a/pkg/workflow/create_entity_helpers.go +++ b/pkg/workflow/create_entity_helpers.go @@ -15,17 +15,31 @@ type CreateParseOptions struct { // CloseOlderConfig holds shared close-older settings across create entity handlers. type CloseOlderConfig struct { - Enabled *string `yaml:"close-older-enabled,omitempty"` // Internal canonical key; populated from entity-specific close-older-* keys before unmarshaling. - Key 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. + // Enabled is intentionally not a YAML field (yaml:"-"): it must not be settable + // directly from workflow frontmatter. It is populated after unmarshaling via + // closeOlderEnabledFromConfigData, keeping each entity's canonical key + // (e.g. close-older-issues) as the sole public YAML surface. + Enabled *string `yaml:"-"` + Key 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. } -func setCloseOlderEnabledAlias(configData map[string]any, sourceKey string) { +// closeOlderEnabledFromConfigData reads the close-older enabled value from sourceKey in +// configData (already normalized to a string form by preprocessBoolFieldAsString) and +// returns a pointer suitable for CloseOlderConfig.Enabled, or nil when sourceKey was not +// set. Intended to be called from postUnmarshal callbacks, once per entity handler. +func closeOlderEnabledFromConfigData(configData map[string]any, sourceKey string) *string { if configData == nil { - return + return nil + } + value, exists := configData[sourceKey] + if !exists { + return nil } - if value, exists := configData[sourceKey]; exists { - configData["close-older-enabled"] = value + str, ok := value.(string) + if !ok { + return nil } + return &str } // parseCreateEntityConfig parses create-* config scaffolding shared by issue/discussion/PR handlers. diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index d045eb274b8..7ca6b026992 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -33,7 +33,7 @@ func (c *Compiler) parseCreateIssuesConfig(outputMap map[string]any) *CreateIssu outputMap, "create-issue", CreateParseOptions{ - BoolFields: []string{"close-older-issues", "close-older-enabled", "group", "footer", "group-by-day"}, + BoolFields: []string{"close-older-issues", "group", "footer", "group-by-day"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -44,11 +44,12 @@ func (c *Compiler) parseCreateIssuesConfig(outputMap map[string]any) *CreateIssu return &CreateIssuesConfig{} }, func(configData map[string]any) bool { - setCloseOlderEnabledAlias(configData, "close-older-issues") coerceStringOrArrayFields(configData, []string{"assignees"}, createIssueLog) return true }, - func(_ map[string]any, config *CreateIssuesConfig, expiresDisabled bool) { + func(configData map[string]any, config *CreateIssuesConfig, expiresDisabled bool) { + config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-issues") + // Set default max if not specified if config.Max == nil { config.Max = defaultIntStr(1) diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 35ef946fbd9..0f571a8260e 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -146,7 +146,7 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea outputMap, "create-pull-request", CreateParseOptions{ - BoolFields: []string{"draft", "allow-empty", "footer", "auto-close-issue", "close-older-pull-requests", "close-older-enabled"}, + BoolFields: []string{"draft", "allow-empty", "footer", "auto-close-issue", "close-older-pull-requests"}, IntFields: []string{"max"}, HandleExpires: true, }, @@ -157,7 +157,6 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea return &CreatePullRequestsConfig{} }, func(configData map[string]any) bool { - setCloseOlderEnabledAlias(configData, "close-older-pull-requests") coerceStringOrArrayFields(configData, createPRStringOrArrayFields, createPRLog) // Pre-process protected-files: supports string enum OR object form {policy, exclude}. @@ -204,6 +203,8 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea return true }, func(configData map[string]any, config *CreatePullRequestsConfig, expiresDisabled bool) { + config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-pull-requests") + if expiresDisabled { createPRLog.Print("Pull request expiration disabled") } diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index 341fecc4706..a66dcc87e70 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -72,6 +72,9 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("assignees", c.Assignees). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddTemplatableBool("group", c.Group). + // Shared CloseOlderConfig.Enabled is remapped here to this handler's + // entity-specific env key name; the other create-* handlers below map the + // same shared field to their own entity-specific keys. AddTemplatableBool("close_older_issues", c.CloseOlderConfig.Enabled). AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddTemplatableBool("group_by_day", c.GroupByDay). @@ -117,6 +120,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("labels", c.Labels). AddStringSlice("allowed_labels", c.AllowedLabels). AddStringSlice("allowed_repos", c.AllowedRepos). + // entity-specific env key name per shared CloseOlderConfig field (see create-issue handler above) AddTemplatableBool("close_older_discussions", c.CloseOlderConfig.Enabled). AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddIfNotEmpty("required_category", c.RequiredCategory). @@ -588,6 +592,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfTrue("recreate_ref", c.RecreateRef). AddIfNotEmpty("patch_format", c.PatchFormat). AddBoolPtr("signed_commits", c.SignedCommits). + // entity-specific env key name per shared CloseOlderConfig field (see create-issue handler above) AddTemplatableBool("close_older_pull_requests", c.CloseOlderConfig.Enabled). AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)) From 5e9aab4b81c191eb7d495d26e44eebd9fb8541f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:41:00 +0000 Subject: [PATCH 6/8] Harden closeOlderEnabledFromConfigData against raw bool input per code review Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/create_entity_helpers.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/workflow/create_entity_helpers.go b/pkg/workflow/create_entity_helpers.go index 38bc39194b5..c85af3cc111 100644 --- a/pkg/workflow/create_entity_helpers.go +++ b/pkg/workflow/create_entity_helpers.go @@ -24,9 +24,11 @@ type CloseOlderConfig struct { } // closeOlderEnabledFromConfigData reads the close-older enabled value from sourceKey in -// configData (already normalized to a string form by preprocessBoolFieldAsString) and -// returns a pointer suitable for CloseOlderConfig.Enabled, or nil when sourceKey was not -// set. Intended to be called from postUnmarshal callbacks, once per entity handler. +// configData (normalized to a string form by preprocessBoolFieldAsString, which callers +// must have already run for sourceKey) and returns a pointer suitable for +// CloseOlderConfig.Enabled, or nil when sourceKey was not set. Also tolerates a raw bool +// (e.g. if called before preprocessing) as a defensive fallback. Intended to be called +// from postUnmarshal callbacks, once per entity handler. func closeOlderEnabledFromConfigData(configData map[string]any, sourceKey string) *string { if configData == nil { return nil @@ -35,11 +37,18 @@ func closeOlderEnabledFromConfigData(configData map[string]any, sourceKey string if !exists { return nil } - str, ok := value.(string) - if !ok { + switch v := value.(type) { + case string: + return &v + case bool: + str := "false" + if v { + str = "true" + } + return &str + default: return nil } - return &str } // parseCreateEntityConfig parses create-* config scaffolding shared by issue/discussion/PR handlers. From 5598aa24ed5a2365ef3c9ef2893c7f83973c1039 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:15:56 +0000 Subject: [PATCH 7/8] Fix staticcheck QF1008 lint errors for embedded CloseOlderConfig selectors Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../create_close_older_config_test.go | 36 +++++++++---------- pkg/workflow/create_discussion.go | 4 +-- pkg/workflow/create_issue.go | 2 +- pkg/workflow/create_pull_request.go | 6 ++-- pkg/workflow/safe_outputs_handler_registry.go | 12 +++---- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/pkg/workflow/create_close_older_config_test.go b/pkg/workflow/create_close_older_config_test.go index cf75e2041af..3efb3f42d62 100644 --- a/pkg/workflow/create_close_older_config_test.go +++ b/pkg/workflow/create_close_older_config_test.go @@ -19,9 +19,9 @@ func TestParseCreateIssuesConfigMapsCloseOlderConfig(t *testing.T) { }) require.NotNil(t, config) - require.NotNil(t, config.CloseOlderConfig.Enabled) - assert.Equal(t, "true", *config.CloseOlderConfig.Enabled) - assert.Equal(t, "issue-key", config.CloseOlderConfig.Key) + require.NotNil(t, config.Enabled) + assert.Equal(t, "true", *config.Enabled) + assert.Equal(t, "issue-key", config.Key) } func TestParseCreateDiscussionsConfigMapsCloseOlderConfig(t *testing.T) { @@ -34,9 +34,9 @@ func TestParseCreateDiscussionsConfigMapsCloseOlderConfig(t *testing.T) { }) require.NotNil(t, config) - require.NotNil(t, config.CloseOlderConfig.Enabled) - assert.Equal(t, "${{ true }}", *config.CloseOlderConfig.Enabled) - assert.Equal(t, "discussion-key", config.CloseOlderConfig.Key) + require.NotNil(t, config.Enabled) + assert.Equal(t, "${{ true }}", *config.Enabled) + assert.Equal(t, "discussion-key", config.Key) } func TestParseCreatePullRequestsConfigMapsCloseOlderConfig(t *testing.T) { @@ -49,9 +49,9 @@ func TestParseCreatePullRequestsConfigMapsCloseOlderConfig(t *testing.T) { }) require.NotNil(t, config) - require.NotNil(t, config.CloseOlderConfig.Enabled) - assert.Equal(t, "true", *config.CloseOlderConfig.Enabled) - assert.Equal(t, "pull-request-key", config.CloseOlderConfig.Key) + require.NotNil(t, config.Enabled) + assert.Equal(t, "true", *config.Enabled) + assert.Equal(t, "pull-request-key", config.Key) } func TestParseCreateIssuesConfigNoCloseOlderWhenAbsent(t *testing.T) { @@ -61,8 +61,8 @@ func TestParseCreateIssuesConfigNoCloseOlderWhenAbsent(t *testing.T) { }) require.NotNil(t, config) - assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") - assert.Empty(t, config.CloseOlderConfig.Key) + assert.Nil(t, config.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.Key) } func TestParseCreateIssuesConfigCloseOlderExplicitFalse(t *testing.T) { @@ -74,8 +74,8 @@ func TestParseCreateIssuesConfigCloseOlderExplicitFalse(t *testing.T) { }) require.NotNil(t, config) - require.NotNil(t, config.CloseOlderConfig.Enabled, "Enabled should be set (not nil) when explicitly false") - assert.Equal(t, "false", *config.CloseOlderConfig.Enabled) + require.NotNil(t, config.Enabled, "Enabled should be set (not nil) when explicitly false") + assert.Equal(t, "false", *config.Enabled) } func TestParseCreateDiscussionsConfigNoCloseOlderWhenAbsent(t *testing.T) { @@ -85,8 +85,8 @@ func TestParseCreateDiscussionsConfigNoCloseOlderWhenAbsent(t *testing.T) { }) require.NotNil(t, config) - assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") - assert.Empty(t, config.CloseOlderConfig.Key) + assert.Nil(t, config.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.Key) } func TestParseCreatePullRequestsConfigNoCloseOlderWhenAbsent(t *testing.T) { @@ -96,8 +96,8 @@ func TestParseCreatePullRequestsConfigNoCloseOlderWhenAbsent(t *testing.T) { }) require.NotNil(t, config) - assert.Nil(t, config.CloseOlderConfig.Enabled, "Enabled should be nil when not set") - assert.Empty(t, config.CloseOlderConfig.Key) + assert.Nil(t, config.Enabled, "Enabled should be nil when not set") + assert.Empty(t, config.Key) } // TestParseCreateIssuesConfigCloseOlderEnabledIsNotAPublicKey verifies that the internal @@ -113,5 +113,5 @@ func TestParseCreateIssuesConfigCloseOlderEnabledIsNotAPublicKey(t *testing.T) { }) require.NotNil(t, config) - assert.Nil(t, config.CloseOlderConfig.Enabled, "close-older-enabled must not be a supported public YAML key") + assert.Nil(t, config.Enabled, "close-older-enabled must not be a supported public YAML key") } diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index a1ccbdda5e4..ec396057994 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -45,7 +45,7 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat }, nil, func(configData map[string]any, config *CreateDiscussionsConfig, expiresDisabled bool) { - config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-discussions") + config.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-discussions") // Set default max if not specified if config.Max == nil { @@ -95,7 +95,7 @@ func (c *Compiler) parseCreateDiscussionsConfig(outputMap map[string]any) *Creat if len(config.AllowedRepos) > 0 { discussionLog.Printf("Allowed repos configured: %v", config.AllowedRepos) } - if config.CloseOlderConfig.Enabled != nil { + if config.Enabled != nil { discussionLog.Print("Close older discussions flag set") if config.RequiredCategory != "" { discussionLog.Printf("Required category for close older discussions: %q", config.RequiredCategory) diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 7ca6b026992..c48a9fca8e2 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -48,7 +48,7 @@ func (c *Compiler) parseCreateIssuesConfig(outputMap map[string]any) *CreateIssu return true }, func(configData map[string]any, config *CreateIssuesConfig, expiresDisabled bool) { - config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-issues") + config.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-issues") // Set default max if not specified if config.Max == nil { diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 0f571a8260e..f5486c048dd 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -25,10 +25,10 @@ func getFallbackAsIssue(config *CreatePullRequestsConfig) bool { // value, including GitHub Actions expressions like "${{ ... }}", is treated as enabled. // Used for compile-time permission calculation. func isCloseOlderPullRequestsEnabled(config *CreatePullRequestsConfig) bool { - if config == nil || config.CloseOlderConfig.Enabled == nil { + if config == nil || config.Enabled == nil { return false } - v := *config.CloseOlderConfig.Enabled + v := *config.Enabled return v != "" && v != "false" && v != "0" } @@ -203,7 +203,7 @@ func (c *Compiler) parseCreatePullRequestsConfig(outputMap map[string]any) *Crea return true }, func(configData map[string]any, config *CreatePullRequestsConfig, expiresDisabled bool) { - config.CloseOlderConfig.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-pull-requests") + config.Enabled = closeOlderEnabledFromConfigData(configData, "close-older-pull-requests") if expiresDisabled { createPRLog.Print("Pull request expiration disabled") diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index a66dcc87e70..dedb5e7fb1b 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -75,8 +75,8 @@ var handlerRegistry = map[string]handlerBuilder{ // Shared CloseOlderConfig.Enabled is remapped here to this handler's // entity-specific env key name; the other create-* handlers below map the // same shared field to their own entity-specific keys. - AddTemplatableBool("close_older_issues", c.CloseOlderConfig.Enabled). - AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). + AddTemplatableBool("close_older_issues", c.Enabled). + AddIfNotEmpty("close_older_key", c.Key). AddTemplatableBool("group_by_day", c.GroupByDay). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", resolveHandlerGitHubToken(c.GitHubApp, "create-issue", c.GitHubToken)). @@ -121,8 +121,8 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_labels", c.AllowedLabels). AddStringSlice("allowed_repos", c.AllowedRepos). // entity-specific env key name per shared CloseOlderConfig field (see create-issue handler above) - AddTemplatableBool("close_older_discussions", c.CloseOlderConfig.Enabled). - AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). + AddTemplatableBool("close_older_discussions", c.Enabled). + AddIfNotEmpty("close_older_key", c.Key). AddIfNotEmpty("required_category", c.RequiredCategory). AddIfPositive("expires", c.Expires). AddBoolPtr("fallback_to_issue", c.FallbackToIssue). @@ -593,8 +593,8 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("patch_format", c.PatchFormat). AddBoolPtr("signed_commits", c.SignedCommits). // entity-specific env key name per shared CloseOlderConfig field (see create-issue handler above) - AddTemplatableBool("close_older_pull_requests", c.CloseOlderConfig.Enabled). - AddIfNotEmpty("close_older_key", c.CloseOlderConfig.Key). + AddTemplatableBool("close_older_pull_requests", c.Enabled). + AddIfNotEmpty("close_older_key", c.Key). AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)) if c.PreCreate { builder. From 774b8e9f0a1f23a575c82c45618c0be261d84f2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:36:07 +0000 Subject: [PATCH 8/8] Merge main, resolving conflicts from footer centralization vs CloseOlderConfig unification Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-drive.lock.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-drive.lock.yml b/.github/workflows/smoke-drive.lock.yml index a8876f8889b..f6eb2203ac2 100644 --- a/.github/workflows/smoke-drive.lock.yml +++ b/.github/workflows/smoke-drive.lock.yml @@ -1809,8 +1809,8 @@ jobs: continue-on-error: true run: | mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall - [ -d /tmp/gh-aw/sandbox/firewall/logs ] && mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ || true - [ -d /tmp/gh-aw/sandbox/firewall/audit ] && mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ || true + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always()