diff --git a/pkg/cli/add_interactive_secrets.go b/pkg/cli/add_interactive_secrets.go index b392d239619..fb768e347b7 100644 --- a/pkg/cli/add_interactive_secrets.go +++ b/pkg/cli/add_interactive_secrets.go @@ -20,14 +20,9 @@ func (c *AddInteractiveConfig) checkExistingSecrets() error { addInteractiveLog.Printf("Could not fetch existing secrets: %v", err) // Continue without error - we'll just assume no secrets exist } else { - // Parse the output - each secret name is on its own line - secretNames := strings.SplitSeq(strings.TrimSpace(string(output)), "\n") - for name := range secretNames { - name = strings.TrimSpace(name) - if name != "" { - c.existingSecrets[name] = true - addInteractiveLog.Printf("Found existing repository secret: %s", name) - } + for _, name := range parseSecretNames(output) { + c.existingSecrets[name] = true + addInteractiveLog.Printf("Found existing repository secret: %s", name) } } @@ -37,13 +32,9 @@ func (c *AddInteractiveConfig) checkExistingSecrets() error { if orgErr != nil { addInteractiveLog.Printf("Could not fetch org secrets (this is expected for personal repos or if org access is restricted): %v", orgErr) } else { - orgSecretNames := strings.SplitSeq(strings.TrimSpace(string(orgOutput)), "\n") - for name := range orgSecretNames { - name = strings.TrimSpace(name) - if name != "" { - c.existingSecrets[name] = true - addInteractiveLog.Printf("Found existing org secret: %s", name) - } + for _, name := range parseSecretNames(orgOutput) { + c.existingSecrets[name] = true + addInteractiveLog.Printf("Found existing org secret: %s", name) } } } @@ -64,6 +55,18 @@ func (c *AddInteractiveConfig) addRepositorySecret(name, value string) error { return nil } +// parseSecretNames parses newline-delimited GitHub API output and returns the +// non-empty, trimmed secret names. +func parseSecretNames(output []byte) []string { + var names []string + for name := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") { + if name = strings.TrimSpace(name); name != "" { + names = append(names, name) + } + } + return names +} + // getSecretInfo returns the secret name and value based on the selected engine // Returns empty value if the secret already exists in the repository func (c *AddInteractiveConfig) getSecretInfo() (name string, value string, err error) { diff --git a/pkg/cli/add_interactive_secrets_test.go b/pkg/cli/add_interactive_secrets_test.go index 2490a65d56f..6dc048168ad 100644 --- a/pkg/cli/add_interactive_secrets_test.go +++ b/pkg/cli/add_interactive_secrets_test.go @@ -169,6 +169,57 @@ func TestAddInteractiveConfig_configureEngineAPISecret_skipSecret(t *testing.T) } } +func TestParseSecretNames(t *testing.T) { + tests := []struct { + name string + input []byte + expected []string + }{ + { + name: "single secret name", + input: []byte("MY_SECRET\n"), + expected: []string{"MY_SECRET"}, + }, + { + name: "multiple secret names", + input: []byte("SECRET_A\nSECRET_B\nSECRET_C"), + expected: []string{"SECRET_A", "SECRET_B", "SECRET_C"}, + }, + { + name: "empty output", + input: []byte(""), + expected: nil, + }, + { + name: "output with only whitespace", + input: []byte(" \n \n"), + expected: nil, + }, + { + name: "names with surrounding whitespace", + input: []byte(" MY_SECRET \n ANOTHER_SECRET "), + expected: []string{"MY_SECRET", "ANOTHER_SECRET"}, + }, + { + name: "output with blank lines interspersed", + input: []byte("FIRST_SECRET\n\nSECOND_SECRET\n\n"), + expected: []string{"FIRST_SECRET", "SECOND_SECRET"}, + }, + { + name: "trailing newline is handled correctly", + input: []byte("SECRET_ONE\nSECRET_TWO\n"), + expected: []string{"SECRET_ONE", "SECRET_TWO"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseSecretNames(tt.input) + assert.Equal(t, tt.expected, result, "parseSecretNames output should match expected") + }) + } +} + func TestAddInteractiveConfig_checkExistingSecrets(t *testing.T) { config := &AddInteractiveConfig{ RepoOverride: "test-owner/test-repo",