diff --git a/pkg/cli/run_workflow_validation.go b/pkg/cli/run_workflow_validation.go index 34dd61a43df..b8f79d755ec 100644 --- a/pkg/cli/run_workflow_validation.go +++ b/pkg/cli/run_workflow_validation.go @@ -15,6 +15,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/workflow" "github.com/goccy/go-yaml" ) @@ -237,7 +238,7 @@ func validateWorkflowInputs(markdownPath string, providedInputs []string) error // Add helpful information about valid inputs if len(workflowInputs) > 0 { var inputDescriptions []string - sortedNames := slices.Sorted(maps.Keys(workflowInputs)) + sortedNames := sliceutil.SortedKeys(workflowInputs) for _, name := range sortedNames { def := workflowInputs[name] required := "" diff --git a/pkg/sliceutil/README.md b/pkg/sliceutil/README.md index b2f7a759a26..3d577dc9614 100644 --- a/pkg/sliceutil/README.md +++ b/pkg/sliceutil/README.md @@ -16,6 +16,7 @@ All functions in this package are pure: they never modify their input. They are | `Map` | `func[T, U any](slice []T, transform func(T) U) []U` | Applies `transform` to every element and returns the results as a new slice | | `MapKeys` | `func[K comparable, V any](m map[K]V) []K` | Converts the keys of a map into a slice; order is not guaranteed | | `FilterMapKeys` | `func[K comparable, V any](m map[K]V, predicate func(K, V) bool) []K` | Returns map keys for which `predicate(key, value)` is `true`; order is not guaranteed | +| `SortedKeys` | `func[K cmp.Ordered, V any](m map[K]V) []K` | Returns the keys of a map in sorted order; K must satisfy `cmp.Ordered` (e.g. `string`, `int`) | | `Any` | `func[T any](slice []T, predicate func(T) bool) bool` | Returns `true` if at least one element satisfies `predicate`; returns `false` for nil or empty slices | | `Deduplicate` | `func[T comparable](slice []T) []T` | Returns a new slice with duplicate elements removed, preserving order of first occurrence | | `MergeUnique` | `func[T comparable](base []T, extra ...T) []T` | Returns a deduplicated slice starting with `base` and appending unseen values from `extra` | @@ -48,6 +49,11 @@ merged := sliceutil.MergeUnique([]string{"a", "b"}, "b", "c") // Exclude values filtered := sliceutil.Exclude([]string{"a", "b", "c"}, "b") // filtered = ["a", "c"] + +// Sorted map keys +m := map[string]int{"banana": 2, "apple": 1} +keys := sliceutil.SortedKeys(m) +// keys = ["apple", "banana"] ``` ## Dependencies @@ -62,7 +68,8 @@ filtered := sliceutil.Exclude([]string{"a", "b", "c"}, "b") - `Any` is implemented via `slices.ContainsFunc` from the standard library. - `Deduplicate`, `MergeUnique`, and `Exclude` use hash sets (`map[T]struct{}`) for O(n) behavior. -- None of these functions sort their output; callers that require sorted results should call `slices.Sort` on the returned slice. +- `SortedKeys` delegates to `slices.Sorted(maps.Keys(m))` from the standard library and returns a new sorted slice each call. +- None of the other functions sort their output; callers that require sorted results should call `slices.Sort` on the returned slice. --- diff --git a/pkg/sliceutil/sliceutil.go b/pkg/sliceutil/sliceutil.go index c719871dc0d..d03321ad8c5 100644 --- a/pkg/sliceutil/sliceutil.go +++ b/pkg/sliceutil/sliceutil.go @@ -2,6 +2,8 @@ package sliceutil import ( + "cmp" + "maps" "slices" "github.com/github/gh-aw/pkg/logger" @@ -55,6 +57,13 @@ func FilterMapKeys[K comparable, V any](m map[K]V, predicate func(K, V) bool) [] return result } +// SortedKeys returns the keys of a map in sorted order. +// K must satisfy cmp.Ordered (e.g. string, int). +// This is a pure function that does not modify the input map. +func SortedKeys[K cmp.Ordered, V any](m map[K]V) []K { + return slices.Sorted(maps.Keys(m)) +} + // Any returns true if at least one element in the slice satisfies the predicate. // Returns false for nil or empty slices. // This is a pure function that does not modify the input slice. diff --git a/pkg/sliceutil/spec_test.go b/pkg/sliceutil/spec_test.go index 68dda22f7bc..07df25dc90a 100644 --- a/pkg/sliceutil/spec_test.go +++ b/pkg/sliceutil/spec_test.go @@ -81,6 +81,34 @@ func TestSpec_PublicAPI_FilterMapKeys(t *testing.T) { }) } +// TestSpec_PublicAPI_SortedKeys validates the documented behavior of SortedKeys +// as described in the sliceutil README.md specification. +func TestSpec_PublicAPI_SortedKeys(t *testing.T) { + t.Run("returns map keys in sorted order", func(t *testing.T) { + m := map[string]int{"banana": 2, "apple": 1, "cherry": 3} + keys := SortedKeys(m) + assert.Equal(t, []string{"apple", "banana", "cherry"}, keys, "SortedKeys should return keys in sorted order") + }) + + t.Run("returns empty slice for empty map", func(t *testing.T) { + m := map[string]int{} + keys := SortedKeys(m) + assert.Empty(t, keys, "SortedKeys of empty map should return empty slice") + }) + + t.Run("returns nil for nil map", func(t *testing.T) { + var m map[string]int + keys := SortedKeys(m) + assert.Nil(t, keys, "SortedKeys of nil map should return nil, not an empty slice") + }) + + t.Run("works with non-string ordered key types", func(t *testing.T) { + m := map[int]string{3: "c", 1: "a", 2: "b"} + keys := SortedKeys(m) + assert.Equal(t, []int{1, 2, 3}, keys, "SortedKeys should sort integer keys numerically") + }) +} + // TestSpec_PublicAPI_Any validates the documented behavior of Any as described // in the sliceutil README.md specification. func TestSpec_PublicAPI_Any(t *testing.T) { diff --git a/pkg/workflow/bots_test.go b/pkg/workflow/bots_test.go index effb0bf438f..b2488d2cb7a 100644 --- a/pkg/workflow/bots_test.go +++ b/pkg/workflow/bots_test.go @@ -201,8 +201,6 @@ Test workflow content with bot and default roles.` // TestMergeBots tests the mergeBots helper function func TestMergeBots(t *testing.T) { - compiler := NewCompiler() - tests := []struct { name string top []string @@ -243,7 +241,7 @@ func TestMergeBots(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := compiler.mergeBots(tt.top, tt.imported) + result := mergeBots(tt.top, tt.imported) assert.Equal(t, tt.expected, result, "mergeBots result mismatch") }) } diff --git a/pkg/workflow/compiler_main_job.go b/pkg/workflow/compiler_main_job.go index f5b83baa166..2b7036ce50a 100644 --- a/pkg/workflow/compiler_main_job.go +++ b/pkg/workflow/compiler_main_job.go @@ -2,7 +2,6 @@ package workflow import ( "fmt" - "maps" "os" "slices" "sort" @@ -13,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/setutil" + "github.com/github/gh-aw/pkg/sliceutil" ) var compilerMainJobLog = logger.New("workflow:compiler_main_job") @@ -109,7 +109,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( // so the agent job gets them transitively through activation // Custom jobs that depend on agent should run AFTER the agent job, not before it if data.Jobs != nil { - for _, jobName := range slices.Sorted(maps.Keys(data.Jobs)) { + for _, jobName := range sliceutil.SortedKeys(data.Jobs) { // Skip built-in jobs as they are handled separately and should not become custom dependencies. if isBuiltinJobName(jobName) { continue diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 1f139e68aa6..563b76806cb 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -428,11 +428,11 @@ func (c *Compiler) extractAdditionalConfigurations( } workflowData.Roles = c.extractRoles(frontmatter) - workflowData.Bots = expandBotNames(c.mergeBots(c.extractBots(frontmatter), importsResult.MergedBots)) + workflowData.Bots = expandBotNames(mergeBots(c.extractBots(frontmatter), importsResult.MergedBots)) workflowData.LabelNames = c.extractLabelNames(frontmatter) workflowData.RateLimit = c.extractRateLimitConfig(frontmatter) - workflowData.SkipRoles = c.mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles) - workflowData.SkipBots = expandBotNames(c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots)) + workflowData.SkipRoles = mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles) + workflowData.SkipBots = expandBotNames(mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots)) workflowData.SkipAuthorAssociations = c.extractSkipAuthorAssociations(frontmatter) workflowData.AllowBotAuthoredTriggerComment = c.extractAllowBotAuthoredTriggerComment(frontmatter) workflowData.ActivationGitHubToken = c.resolveActivationGitHubToken(frontmatter, importsResult) diff --git a/pkg/workflow/compiler_yaml_step_conversion.go b/pkg/workflow/compiler_yaml_step_conversion.go index fccca59463a..ec946c3166b 100644 --- a/pkg/workflow/compiler_yaml_step_conversion.go +++ b/pkg/workflow/compiler_yaml_step_conversion.go @@ -2,7 +2,6 @@ package workflow import ( "fmt" - "maps" "os" "slices" "strings" @@ -10,6 +9,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/goccy/go-yaml" ) @@ -153,7 +153,7 @@ func (c *Compiler) renderStepFromMap(out *strings.Builder, step map[string]any, case map[string]any: // For complex fields like "with" or "env" — sort keys for stable output. fmt.Fprintf(out, "%s:\n", field) - for _, key := range slices.Sorted(maps.Keys(v)) { + for _, key := range sliceutil.SortedKeys(v) { if field == "env" { fmt.Fprintf(out, "%s %s: %s\n", indent, key, formatStepEnvValueForYAML(v[key])) } else { @@ -194,7 +194,7 @@ func (c *Compiler) renderStepFromMap(out *strings.Builder, step map[string]any, case map[string]any: // Sort keys for stable output. fmt.Fprintf(out, "%s:\n", field) - for _, key := range slices.Sorted(maps.Keys(v)) { + for _, key := range sliceutil.SortedKeys(v) { if field == "env" { fmt.Fprintf(out, "%s %s: %s\n", indent, key, formatStepEnvValueForYAML(v[key])) } else { diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index ec220013629..a551440c88b 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -4,13 +4,12 @@ import ( _ "embed" "encoding/json" "fmt" - "maps" - "slices" "sort" "strings" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -359,7 +358,7 @@ func getEcosystemDomains(category string) []string { domainMap[d] = struct{}{} } } - result := slices.Sorted(maps.Keys(domainMap)) + result := sliceutil.SortedKeys(domainMap) return result } @@ -425,7 +424,7 @@ func getDomainsFromRuntimes(runtimes map[string]any) []string { } } - return slices.Sorted(maps.Keys(domainMap)) + return sliceutil.SortedKeys(domainMap) } // GetAllowedDomains returns the allowed domains from network permissions. @@ -520,7 +519,7 @@ func GetAllowedDomains(network *NetworkPermissions) []string { } } - return slices.Sorted(maps.Keys(domainMap)) + return sliceutil.SortedKeys(domainMap) } // ecosystemPriority defines the order in which ecosystems are checked by GetDomainEcosystem. @@ -729,7 +728,7 @@ func mergeDomainsWithNetworkToolsAndRuntimes(defaultDomains []string, network *N } } - domains := slices.Sorted(maps.Keys(domainMap)) + domains := sliceutil.SortedKeys(domainMap) // Join with commas for AWF --allow-domains flag return strings.Join(domains, ",") @@ -856,7 +855,7 @@ func GetBlockedDomains(network *NetworkPermissions) []string { } } - return slices.Sorted(maps.Keys(domainMap)) + return sliceutil.SortedKeys(domainMap) } // formatBlockedDomains formats blocked domains as a comma-separated string suitable for AWF's --block-domains flag @@ -922,7 +921,7 @@ func mergeAPITargetDomains(domainsStr string, apiTarget string) string { domainMap[d] = struct{}{} } - return strings.Join(slices.Sorted(maps.Keys(domainMap)), ",") + return strings.Join(sliceutil.SortedKeys(domainMap), ",") } // computeAllowedDomainsForSanitization computes the allowed domains for sanitization @@ -1009,7 +1008,7 @@ func expandAllowedDomains(entries []string) []string { domainMap[entry] = struct{}{} } } - return slices.Sorted(maps.Keys(domainMap)) + return sliceutil.SortedKeys(domainMap) } // computeExpandedAllowedDomainsForSanitization computes the allowed domains for URL sanitization, @@ -1050,5 +1049,5 @@ func (c *Compiler) computeExpandedAllowedDomainsForSanitization(data *WorkflowDa domainMap["github.com"] = struct{}{} // Produce a sorted, comma-separated result - return strings.Join(slices.Sorted(maps.Keys(domainMap)), ","), nil + return strings.Join(sliceutil.SortedKeys(domainMap), ","), nil } diff --git a/pkg/workflow/map_helpers.go b/pkg/workflow/map_helpers.go index 9b1b90b7712..7e3804816bb 100644 --- a/pkg/workflow/map_helpers.go +++ b/pkg/workflow/map_helpers.go @@ -18,7 +18,6 @@ // // Map Operations: // - excludeMapKeys() - Create new map excluding specified keys -// - sortedMapKeys() - Return sorted keys of a map[string]string // // For type conversion utilities, use pkg/typeutil directly: // - typeutil.ParseIntValue() - Strictly parse numeric types to int; returns (value, ok). @@ -34,9 +33,6 @@ package workflow import ( - "maps" - "slices" - "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/setutil" ) @@ -66,9 +62,3 @@ func excludeMapKeys(original map[string]any, excludeKeys ...string) map[string]a } return result } - -// sortedMapKeys returns the keys of a map[string]string in sorted order. -// Used to produce deterministic output when writing environment variables. -func sortedMapKeys(m map[string]string) []string { - return slices.Sorted(maps.Keys(m)) -} diff --git a/pkg/workflow/mcp_renderer_github.go b/pkg/workflow/mcp_renderer_github.go index 70fdc16a368..a56b37d9c66 100644 --- a/pkg/workflow/mcp_renderer_github.go +++ b/pkg/workflow/mcp_renderer_github.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/sliceutil" ) // RenderGitHubMCP generates the GitHub MCP server configuration @@ -189,7 +190,7 @@ func (r *MCPConfigRendererUnified) renderGitHubTOML(yaml *strings.Builder, githu ) // Write environment variables in sorted order for deterministic output - envKeys := sortedMapKeys(envVars) + envKeys := sliceutil.SortedKeys(envVars) writeTOMLInlineStringMapSection(yaml, " ", "env", envVars) diff --git a/pkg/workflow/mcp_renderer_section_helpers.go b/pkg/workflow/mcp_renderer_section_helpers.go index 7a873368b7a..62bfce7ceec 100644 --- a/pkg/workflow/mcp_renderer_section_helpers.go +++ b/pkg/workflow/mcp_renderer_section_helpers.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "strings" + + "github.com/github/gh-aw/pkg/sliceutil" ) func writeJSONStringMapEntries(yaml *strings.Builder, values map[string]string, indent string) { - for i, key := range sortedMapKeys(values) { + for i, key := range sliceutil.SortedKeys(values) { comma := "," if i == len(values)-1 { comma = "" @@ -36,7 +38,7 @@ func writeJSONStringMapSection(yaml *strings.Builder, indent, name string, value func writeTOMLInlineStringMapSection(yaml *strings.Builder, indent, name string, values map[string]string) { fmt.Fprintf(yaml, "%s%s = { ", indent, name) - for i, key := range sortedMapKeys(values) { + for i, key := range sliceutil.SortedKeys(values) { if i > 0 { yaml.WriteString(", ") } diff --git a/pkg/workflow/mcp_setup_generator.go b/pkg/workflow/mcp_setup_generator.go index 0283485e5bd..9ae1c12c017 100644 --- a/pkg/workflow/mcp_setup_generator.go +++ b/pkg/workflow/mcp_setup_generator.go @@ -353,7 +353,7 @@ func buildSafeOutputsConfigRuntimeEnvVars(safeOutputConfig string) ([]string, ma // Prefix secret env vars to avoid colliding with reserved/known step env var names. addEnvValue(safeOutputsSecretEnvPrefix+k, v) } - return sortedMapKeys(envValues), envValues + return sliceutil.SortedKeys(envValues), envValues } func buildSafeOutputsConfigRuntimeData(safeOutputConfig string) (string, []string, map[string]string) { diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index 8fe8a86bbf4..214b96ae4bd 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -623,31 +623,29 @@ func (c *Compiler) extractAllowBotAuthoredTriggerComment(frontmatter map[string] return false } -// mergeSkipRoles merges top-level skip-roles with imported skip-roles (union) -func (c *Compiler) mergeSkipRoles(topSkipRoles []string, importedSkipRoles []string) []string { - result := sliceutil.MergeUnique(topSkipRoles, importedSkipRoles...) +// mergeUniqueLogged merges top-level and imported string slices using a union (deduplication preserving +// order), logging the result at debug level under the given label. +func mergeUniqueLogged(label string, top []string, imported []string) []string { + result := sliceutil.MergeUnique(top, imported...) if len(result) > 0 { - roleLog.Printf("Merged %s: %v (top=%d, imported=%d, total=%d)", "skip-roles", result, len(topSkipRoles), len(importedSkipRoles), len(result)) + roleLog.Printf("Merged %s: %v (top=%d, imported=%d, total=%d)", label, result, len(top), len(imported), len(result)) } return result } +// mergeSkipRoles merges top-level skip-roles with imported skip-roles (union) +func mergeSkipRoles(topSkipRoles []string, importedSkipRoles []string) []string { + return mergeUniqueLogged("skip-roles", topSkipRoles, importedSkipRoles) +} + // mergeSkipBots merges top-level skip-bots with imported skip-bots (union) -func (c *Compiler) mergeSkipBots(topSkipBots []string, importedSkipBots []string) []string { - result := sliceutil.MergeUnique(topSkipBots, importedSkipBots...) - if len(result) > 0 { - roleLog.Printf("Merged %s: %v (top=%d, imported=%d, total=%d)", "skip-bots", result, len(topSkipBots), len(importedSkipBots), len(result)) - } - return result +func mergeSkipBots(topSkipBots []string, importedSkipBots []string) []string { + return mergeUniqueLogged("skip-bots", topSkipBots, importedSkipBots) } // mergeBots merges top-level bots with imported bots (union) -func (c *Compiler) mergeBots(topBots []string, importedBots []string) []string { - result := sliceutil.MergeUnique(topBots, importedBots...) - if len(result) > 0 { - roleLog.Printf("Merged %s: %v (top=%d, imported=%d, total=%d)", "bots", result, len(topBots), len(importedBots), len(result)) - } - return result +func mergeBots(topBots []string, importedBots []string) []string { + return mergeUniqueLogged("bots", topBots, importedBots) } // extractActivationGitHubToken extracts the 'github-token' field from the 'on:' section of frontmatter. diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index b9bbbb008c6..25b668a5f37 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" ) var safeOutputsAppLog = logger.New("workflow:safe_outputs_app") @@ -293,7 +294,7 @@ func (c *Compiler) buildGitHubAppTokenMintStepWithMeta(app *GitHubAppConfig, per } // Extract and sort keys for deterministic ordering - keys := sortedMapKeys(permissionFields) + keys := sliceutil.SortedKeys(permissionFields) // Add permissions in sorted order for _, key := range keys { diff --git a/pkg/workflow/safe_outputs_config_helpers.go b/pkg/workflow/safe_outputs_config_helpers.go index 31dff0a84f1..34ae734a16a 100644 --- a/pkg/workflow/safe_outputs_config_helpers.go +++ b/pkg/workflow/safe_outputs_config_helpers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -23,7 +24,7 @@ func buildNormalizedSortedJSON(names []string, valueFn func(string) string) (str values[normalizedName] = valueFn(normalizedName) } - keys := sortedMapKeys(values) + keys := sliceutil.SortedKeys(values) ordered := make(map[string]string, len(keys)) for _, k := range keys { diff --git a/pkg/workflow/safe_scripts.go b/pkg/workflow/safe_scripts.go index f4d51c86846..13894bc3c7e 100644 --- a/pkg/workflow/safe_scripts.go +++ b/pkg/workflow/safe_scripts.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -104,7 +105,7 @@ func buildCustomSafeOutputScriptsJSON(data *WorkflowData) string { } // Sort keys for deterministic output - keys := sortedMapKeys(scriptMapping) + keys := sliceutil.SortedKeys(scriptMapping) ordered := make(map[string]string, len(keys)) for _, k := range keys { diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go index 598069abd78..00d4f4ae03d 100644 --- a/pkg/workflow/secret_extraction.go +++ b/pkg/workflow/secret_extraction.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" ) var secretLog = logger.New("workflow:secret_extraction") @@ -120,7 +121,7 @@ func ReplaceSecretsWithEnvVars(value string, secrets map[string]string) string { // to both "DD_APPLICATION_KEY" and "DD_APP_KEY"), the alphabetically first key is // processed first and its replacement wins; subsequent keys find the expression // already replaced and are no-ops. This ensures stable lock-file output across runs. - for _, varName := range sortedMapKeys(secrets) { + for _, varName := range sliceutil.SortedKeys(secrets) { secretExpr := secrets[varName] // Replace ${{ secrets.VAR }} with \${VAR} (backslash-escaped for copilot JSON config) result = strings.ReplaceAll(result, secretExpr, "\\${"+varName+"}") @@ -138,7 +139,7 @@ func ReplaceSecretsWithBashVars(value string) string { result := value secrets := ExtractSecretsFromValue(value) // Sort keys for deterministic output; see ReplaceSecretsWithEnvVars for rationale. - for _, varName := range sortedMapKeys(secrets) { + for _, varName := range sliceutil.SortedKeys(secrets) { secretExpr := secrets[varName] result = strings.ReplaceAll(result, secretExpr, "${"+varName+"}") } @@ -326,14 +327,14 @@ func ReplaceTemplateExpressionsWithEnvVars(value string) string { // Extract and replace secrets — sort keys for deterministic output; see // ReplaceSecretsWithEnvVars for rationale. secrets := ExtractSecretsFromValue(value) - for _, varName := range sortedMapKeys(secrets) { + for _, varName := range sliceutil.SortedKeys(secrets) { secretExpr := secrets[varName] result = strings.ReplaceAll(result, secretExpr, "\\${"+varName+"}") } // Extract and replace env vars — sort keys for deterministic output. envVars := ExtractEnvExpressionsFromValue(value) - for _, varName := range sortedMapKeys(envVars) { + for _, varName := range sliceutil.SortedKeys(envVars) { envExpr := envVars[varName] result = strings.ReplaceAll(result, envExpr, "\\${"+varName+"}") } diff --git a/pkg/workflow/yaml.go b/pkg/workflow/yaml.go index 14bb09fc32b..bcccf08fdf6 100644 --- a/pkg/workflow/yaml.go +++ b/pkg/workflow/yaml.go @@ -95,6 +95,7 @@ import ( "sync" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/goccy/go-yaml" ) @@ -393,7 +394,7 @@ func recursivelyOrderYAMLValue(value any) any { return orderedData case map[string]string: orderedData := make(yaml.MapSlice, 0, len(v)) - for _, key := range sortedMapKeys(v) { + for _, key := range sliceutil.SortedKeys(v) { orderedData = append(orderedData, yaml.MapItem{Key: key, Value: v[key]}) } return orderedData