diff --git a/pkg/cli/add_workflow_resolution.go b/pkg/cli/add_workflow_resolution.go index fc35cdd060f..734a42722f6 100644 --- a/pkg/cli/add_workflow_resolution.go +++ b/pkg/cli/add_workflow_resolution.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/sliceutil" ) var resolutionLog = logger.New("cli:add_workflow_resolution") @@ -59,7 +60,7 @@ func ResolveWorkflows(workflows []string, verbose bool) (*ResolvedWorkflows, err } // Parse workflow specifications - parsedSpecs := []*WorkflowSpec{} + parsedSpecs := make([]*WorkflowSpec, 0, len(workflows)) for _, workflow := range workflows { spec, err := parseWorkflowSpec(workflow) @@ -97,13 +98,9 @@ func ResolveWorkflows(workflows []string, verbose bool) (*ResolvedWorkflows, err // If we can't determine the current repository, proceed without the check // Check if any workflow specs contain wildcards (local only) - hasWildcard := false - for _, spec := range parsedSpecs { - if spec.IsWildcard { - hasWildcard = true - break - } - } + hasWildcard := sliceutil.Any(parsedSpecs, func(spec *WorkflowSpec) bool { + return spec.IsWildcard + }) // Expand wildcards for local workflows only if hasWildcard { diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index eb9de5f1538..0230f08d19d 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -11,6 +11,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/repoutil" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -159,16 +160,9 @@ func getMissingRequiredSecrets(requirements []SecretRequirement, existingSecrets continue } - exists := existingSecrets[req.Name] - if !exists { - // Check alternatives - for _, alt := range req.AlternativeEnvVars { - if existingSecrets[alt] { - exists = true - break - } - } - } + exists := existingSecrets[req.Name] || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { + return existingSecrets[alt] + }) if !exists { missing = append(missing, req) } @@ -574,16 +568,9 @@ func displayMissingSecrets(requirements []SecretRequirement, repoSlug string, ex for _, req := range requirements { // Check if secret exists - exists := existingSecrets[req.Name] - if !exists { - // Check alternatives - for _, alt := range req.AlternativeEnvVars { - if existingSecrets[alt] { - exists = true - break - } - } - } + exists := existingSecrets[req.Name] || sliceutil.Any(req.AlternativeEnvVars, func(alt string) bool { + return existingSecrets[alt] + }) if !exists { if req.Optional { diff --git a/pkg/sliceutil/sliceutil.go b/pkg/sliceutil/sliceutil.go index 5f61c1a7513..d697317a40f 100644 --- a/pkg/sliceutil/sliceutil.go +++ b/pkg/sliceutil/sliceutil.go @@ -56,6 +56,18 @@ func FilterMapKeys[K comparable, V any](m map[K]V, predicate func(K, V) bool) [] return result } +// 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. +func Any[T any](slice []T, predicate func(T) bool) bool { + for _, item := range slice { + if predicate(item) { + return true + } + } + return false +} + // Deduplicate returns a new slice with duplicate elements removed. // The order of first occurrence is preserved. // This is a pure function that does not modify the input slice. diff --git a/pkg/sliceutil/sliceutil_test.go b/pkg/sliceutil/sliceutil_test.go index 1b34c00cb1e..b8129099bdc 100644 --- a/pkg/sliceutil/sliceutil_test.go +++ b/pkg/sliceutil/sliceutil_test.go @@ -110,3 +110,88 @@ func TestContains_Duplicates(t *testing.T) { } assert.Equal(t, 3, count, "should count all occurrences of duplicate item") } + +func TestAny(t *testing.T) { + tests := []struct { + name string + slice []int + predicate func(int) bool + expected bool + }{ + { + name: "at least one element matches", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(x int) bool { return x > 3 }, + expected: true, + }, + { + name: "no element matches", + slice: []int{1, 2, 3}, + predicate: func(x int) bool { return x > 10 }, + expected: false, + }, + { + name: "empty slice returns false", + slice: []int{}, + predicate: func(x int) bool { return true }, + expected: false, + }, + { + name: "nil slice returns false", + slice: nil, + predicate: func(x int) bool { return true }, + expected: false, + }, + { + name: "single element matches", + slice: []int{42}, + predicate: func(x int) bool { return x == 42 }, + expected: true, + }, + { + name: "single element does not match", + slice: []int{42}, + predicate: func(x int) bool { return x == 0 }, + expected: false, + }, + { + name: "all elements match", + slice: []int{2, 4, 6, 8}, + predicate: func(x int) bool { return x%2 == 0 }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Any(tt.slice, tt.predicate) + assert.Equal(t, tt.expected, result, + "Any should return %v for slice %v", tt.expected, tt.slice) + }) + } +} + +func TestAny_Strings(t *testing.T) { + secrets := map[string]bool{"SECRET_A": true, "SECRET_B": false} + + // Mirrors the pattern used in engine_secrets.go + exists := Any([]string{"SECRET_A", "SECRET_C"}, func(alt string) bool { + return secrets[alt] + }) + assert.True(t, exists, "Any should return true when one alternative secret exists") + + notExists := Any([]string{"SECRET_C", "SECRET_D"}, func(alt string) bool { + return secrets[alt] + }) + assert.False(t, notExists, "Any should return false when no alternative secret exists") +} + +func TestAny_StopsEarly(t *testing.T) { + callCount := 0 + slice := []int{1, 2, 3, 4, 5} + Any(slice, func(x int) bool { + callCount++ + return x == 2 // matches at index 1 + }) + assert.Equal(t, 2, callCount, "Any should stop evaluating after first match") +}