From 2d4e2dc78a442b04c71cba0ad5596faf664e640e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:45:40 +0000 Subject: [PATCH 1/3] Initial plan From 42c6353ac9b84771c638bd64abde1b362d39d063 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:57:47 +0000 Subject: [PATCH 2/3] Create shared secret extraction utilities and refactor existing code Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/secrets.go | 14 +- pkg/workflow/mcp-config.go | 82 +----- pkg/workflow/mcp_http_headers_test.go | 6 +- pkg/workflow/secret_extraction.go | 112 +++++++++ pkg/workflow/secret_extraction_test.go | 335 +++++++++++++++++++++++++ 5 files changed, 455 insertions(+), 94 deletions(-) create mode 100644 pkg/workflow/secret_extraction.go create mode 100644 pkg/workflow/secret_extraction_test.go diff --git a/pkg/cli/secrets.go b/pkg/cli/secrets.go index 41ea88e392..74f3c7871f 100644 --- a/pkg/cli/secrets.go +++ b/pkg/cli/secrets.go @@ -5,20 +5,15 @@ import ( "fmt" "os" "os/exec" - "regexp" "strings" "github.com/githubnext/gh-aw/pkg/logger" "github.com/githubnext/gh-aw/pkg/parser" + "github.com/githubnext/gh-aw/pkg/workflow" ) var secretsLog = logger.New("cli:secrets") -// Pre-compiled regexes for secret extraction (performance optimization) -var ( - secretPattern = regexp.MustCompile(`\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*(?:\|\|.*?)?\s*\}\}`) -) - // SecretInfo contains information about a required secret type SecretInfo struct { Name string // Secret name (e.g., "DD_API_KEY") @@ -69,12 +64,7 @@ func checkSecretExists(secretName string) (bool, error) { // // "${{ secrets.DD_SITE || 'datadoghq.com' }}" -> "DD_SITE" func extractSecretName(value string) string { - // Match pattern: ${{ secrets.SECRET_NAME }} or ${{ secrets.SECRET_NAME || 'default' }} - matches := secretPattern.FindStringSubmatch(value) - if len(matches) >= 2 { - return matches[1] - } - return "" + return workflow.ExtractSecretName(value) } // extractSecretsFromConfig extracts all required secrets from an MCP server config diff --git a/pkg/workflow/mcp-config.go b/pkg/workflow/mcp-config.go index ee800fbf98..6ac09adbce 100644 --- a/pkg/workflow/mcp-config.go +++ b/pkg/workflow/mcp-config.go @@ -300,7 +300,7 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma // Extract secrets from headers for HTTP MCP tools (copilot engine only) var headerSecrets map[string]string if mcpConfig.Type == "http" && renderer.RequiresCopilotFields { - headerSecrets = extractSecretsFromHeaders(mcpConfig.Headers) + headerSecrets = ExtractSecretsFromMap(mcpConfig.Headers) } // Determine properties based on type @@ -545,7 +545,7 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma // Replace secret expressions with env var references for copilot headerValue := mcpConfig.Headers[headerKey] if renderer.RequiresCopilotFields && len(headerSecrets) > 0 { - headerValue = replaceSecretsWithEnvVars(headerValue, headerSecrets) + headerValue = ReplaceSecretsWithEnvVars(headerValue, headerSecrets) } fmt.Fprintf(yaml, "%s \"%s\": \"%s\"%s\n", renderer.IndentLevel, headerKey, headerValue, headerComma) @@ -650,82 +650,6 @@ func (m MapToolConfig) GetAny(key string) (any, bool) { return value, exists } -// extractSecretsFromValue extracts GitHub Actions secret expressions from a string value -// Returns a map of environment variable names to their secret expressions -// Example: "${{ secrets.DD_API_KEY }}" -> {"DD_API_KEY": "${{ secrets.DD_API_KEY }}"} -// Example: "${{ secrets.DD_SITE || 'datadoghq.com' }}" -> {"DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}"} -func extractSecretsFromValue(value string) map[string]string { - secrets := make(map[string]string) - - // Pattern to match ${{ secrets.VARIABLE_NAME }} or ${{ secrets.VARIABLE_NAME || 'default' }} - // We need to extract the variable name and the full expression - start := 0 - for { - // Find the start of an expression - startIdx := strings.Index(value[start:], "${{ secrets.") - if startIdx == -1 { - break - } - startIdx += start - - // Find the end of the expression - endIdx := strings.Index(value[startIdx:], "}}") - if endIdx == -1 { - break - } - endIdx += startIdx + 2 // Include the closing }} - - // Extract the full expression - fullExpr := value[startIdx:endIdx] - - // Extract the variable name from "secrets.VARIABLE_NAME" or "secrets.VARIABLE_NAME ||" - secretsPart := strings.TrimPrefix(fullExpr, "${{ secrets.") - secretsPart = strings.TrimSuffix(secretsPart, "}}") - secretsPart = strings.TrimSpace(secretsPart) - - // Find the variable name (everything before space, ||, or end) - varName := secretsPart - if spaceIdx := strings.IndexAny(varName, " |"); spaceIdx != -1 { - varName = varName[:spaceIdx] - } - - // Store the variable name and full expression - if varName != "" { - secrets[varName] = fullExpr - } - - start = endIdx - } - - return secrets -} - -// extractSecretsFromHeaders extracts all secrets from HTTP MCP headers -// Returns a map of environment variable names to their secret expressions -func extractSecretsFromHeaders(headers map[string]string) map[string]string { - allSecrets := make(map[string]string) - - for _, headerValue := range headers { - secrets := extractSecretsFromValue(headerValue) - for varName, expr := range secrets { - allSecrets[varName] = expr - } - } - - return allSecrets -} - -// replaceSecretsWithEnvVars replaces secret expressions in a value with environment variable references -// Example: "${{ secrets.DD_API_KEY }}" -> "\${DD_API_KEY}" -func replaceSecretsWithEnvVars(value string, secrets map[string]string) string { - result := value - for varName, secretExpr := range secrets { - // Replace ${{ secrets.VAR }} with \${VAR} (backslash-escaped for copilot JSON config) - result = strings.ReplaceAll(result, secretExpr, "\\${"+varName+"}") - } - return result -} - // collectHTTPMCPHeaderSecrets collects all secrets from HTTP MCP tool headers // Returns a map of environment variable names to their secret expressions func collectHTTPMCPHeaderSecrets(tools map[string]any) map[string]string { @@ -737,7 +661,7 @@ func collectHTTPMCPHeaderSecrets(tools map[string]any) map[string]string { if hasMcp, mcpType := hasMCPConfig(toolConfig); hasMcp && mcpType == "http" { // Extract MCP config to get headers if mcpConfig, err := getMCPConfig(toolConfig, toolName); err == nil { - secrets := extractSecretsFromHeaders(mcpConfig.Headers) + secrets := ExtractSecretsFromMap(mcpConfig.Headers) for varName, expr := range secrets { allSecrets[varName] = expr } diff --git a/pkg/workflow/mcp_http_headers_test.go b/pkg/workflow/mcp_http_headers_test.go index 43ae459ce5..a977da6be1 100644 --- a/pkg/workflow/mcp_http_headers_test.go +++ b/pkg/workflow/mcp_http_headers_test.go @@ -47,7 +47,7 @@ func TestExtractSecretsFromValue(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := extractSecretsFromValue(tt.value) + result := ExtractSecretsFromValue(tt.value) if len(result) != len(tt.expected) { t.Errorf("Expected %d secrets, got %d", len(tt.expected), len(result)) @@ -72,7 +72,7 @@ func TestExtractSecretsFromHeaders(t *testing.T) { "Static": "no-secrets-here", } - result := extractSecretsFromHeaders(headers) + result := ExtractSecretsFromMap(headers) expected := map[string]string{ "DD_API_KEY": "${{ secrets.DD_API_KEY }}", @@ -134,7 +134,7 @@ func TestReplaceSecretsWithEnvVars(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := replaceSecretsWithEnvVars(tt.value, tt.secrets) + result := ReplaceSecretsWithEnvVars(tt.value, tt.secrets) if result != tt.expected { t.Errorf("Expected %q, got %q", tt.expected, result) } diff --git a/pkg/workflow/secret_extraction.go b/pkg/workflow/secret_extraction.go new file mode 100644 index 0000000000..c1f19fc623 --- /dev/null +++ b/pkg/workflow/secret_extraction.go @@ -0,0 +1,112 @@ +package workflow + +import ( + "regexp" + "strings" +) + +// Pre-compiled regex for secret extraction (performance optimization) +// Matches: ${{ secrets.SECRET_NAME }} or ${{ secrets.SECRET_NAME || 'default' }} +var secretExprPattern = regexp.MustCompile(`\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*(?:\|\|.*?)?\s*\}\}`) + +// SecretExpression represents a parsed secret expression +type SecretExpression struct { + VarName string // The secret variable name (e.g., "DD_API_KEY") + FullExpr string // The full expression (e.g., "${{ secrets.DD_API_KEY }}") +} + +// ExtractSecretName extracts just the secret name from a GitHub Actions expression +// Examples: +// - "${{ secrets.DD_API_KEY }}" -> "DD_API_KEY" +// - "${{ secrets.DD_SITE || 'datadoghq.com' }}" -> "DD_SITE" +// - "plain value" -> "" +func ExtractSecretName(value string) string { + matches := secretExprPattern.FindStringSubmatch(value) + if len(matches) >= 2 { + return matches[1] + } + return "" +} + +// ExtractSecretsFromValue extracts all GitHub Actions secret expressions from a string value +// Returns a map of environment variable names to their full secret expressions +// Examples: +// - "${{ secrets.DD_API_KEY }}" -> {"DD_API_KEY": "${{ secrets.DD_API_KEY }}"} +// - "${{ secrets.DD_SITE || 'datadoghq.com' }}" -> {"DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}"} +// - "Bearer ${{ secrets.TOKEN }}" -> {"TOKEN": "${{ secrets.TOKEN }}"} +func ExtractSecretsFromValue(value string) map[string]string { + secrets := make(map[string]string) + + // Pattern to match ${{ secrets.VARIABLE_NAME }} or ${{ secrets.VARIABLE_NAME || 'default' }} + // We need to extract the variable name and the full expression + start := 0 + for { + // Find the start of an expression + startIdx := strings.Index(value[start:], "${{ secrets.") + if startIdx == -1 { + break + } + startIdx += start + + // Find the end of the expression + endIdx := strings.Index(value[startIdx:], "}}") + if endIdx == -1 { + break + } + endIdx += startIdx + 2 // Include the closing }} + + // Extract the full expression + fullExpr := value[startIdx:endIdx] + + // Extract the variable name from "secrets.VARIABLE_NAME" or "secrets.VARIABLE_NAME ||" + secretsPart := strings.TrimPrefix(fullExpr, "${{ secrets.") + secretsPart = strings.TrimSuffix(secretsPart, "}}") + secretsPart = strings.TrimSpace(secretsPart) + + // Find the variable name (everything before space, ||, or end) + varName := secretsPart + if spaceIdx := strings.IndexAny(varName, " |"); spaceIdx != -1 { + varName = varName[:spaceIdx] + } + + // Store the variable name and full expression + if varName != "" { + secrets[varName] = fullExpr + } + + start = endIdx + } + + return secrets +} + +// ExtractSecretsFromMap extracts all secrets from a map of string values +// Returns a map of environment variable names to their full secret expressions +// Example: +// +// Input: {"DD_API_KEY": "${{ secrets.DD_API_KEY }}", "DD_SITE": "${{ secrets.DD_SITE || 'default' }}"} +// Output: {"DD_API_KEY": "${{ secrets.DD_API_KEY }}", "DD_SITE": "${{ secrets.DD_SITE || 'default' }}"} +func ExtractSecretsFromMap(values map[string]string) map[string]string { + allSecrets := make(map[string]string) + + for _, value := range values { + secrets := ExtractSecretsFromValue(value) + for varName, expr := range secrets { + allSecrets[varName] = expr + } + } + + return allSecrets +} + +// ReplaceSecretsWithEnvVars replaces secret expressions in a value with environment variable references +// Example: "${{ secrets.DD_API_KEY }}" -> "\${DD_API_KEY}" +// The backslash is used to escape the ${} for proper JSON rendering in Copilot configs +func ReplaceSecretsWithEnvVars(value string, secrets map[string]string) string { + result := value + for varName, secretExpr := range secrets { + // Replace ${{ secrets.VAR }} with \${VAR} (backslash-escaped for copilot JSON config) + result = strings.ReplaceAll(result, secretExpr, "\\${"+varName+"}") + } + return result +} diff --git a/pkg/workflow/secret_extraction_test.go b/pkg/workflow/secret_extraction_test.go new file mode 100644 index 0000000000..8d3346dd04 --- /dev/null +++ b/pkg/workflow/secret_extraction_test.go @@ -0,0 +1,335 @@ +package workflow + +import ( + "testing" +) + +// TestSharedExtractSecretName tests the shared ExtractSecretName utility function +func TestSharedExtractSecretName(t *testing.T) { + tests := []struct { + name string + value string + expected string + }{ + { + name: "simple secret", + value: "${{ secrets.DD_API_KEY }}", + expected: "DD_API_KEY", + }, + { + name: "secret with default value", + value: "${{ secrets.DD_SITE || 'datadoghq.com' }}", + expected: "DD_SITE", + }, + { + name: "secret with spaces", + value: "${{ secrets.API_TOKEN }}", + expected: "API_TOKEN", + }, + { + name: "bearer token", + value: "Bearer ${{ secrets.TAVILY_API_KEY }}", + expected: "TAVILY_API_KEY", + }, + { + name: "no secret", + value: "plain value", + expected: "", + }, + { + name: "empty value", + value: "", + expected: "", + }, + { + name: "secret with underscore", + value: "${{ secrets.MY_SECRET_KEY }}", + expected: "MY_SECRET_KEY", + }, + { + name: "secret with numbers", + value: "${{ secrets.API_KEY_123 }}", + expected: "API_KEY_123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractSecretName(tt.value) + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +// TestSharedExtractSecretsFromValue tests the shared ExtractSecretsFromValue utility function +func TestSharedExtractSecretsFromValue(t *testing.T) { + tests := []struct { + name string + value string + expected map[string]string + }{ + { + name: "simple secret", + value: "${{ secrets.DD_API_KEY }}", + expected: map[string]string{ + "DD_API_KEY": "${{ secrets.DD_API_KEY }}", + }, + }, + { + name: "secret with default value", + value: "${{ secrets.DD_SITE || 'datadoghq.com' }}", + expected: map[string]string{ + "DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}", + }, + }, + { + name: "bearer token", + value: "Bearer ${{ secrets.TAVILY_API_KEY }}", + expected: map[string]string{ + "TAVILY_API_KEY": "${{ secrets.TAVILY_API_KEY }}", + }, + }, + { + name: "multiple secrets in one value", + value: "${{ secrets.KEY1 }} and ${{ secrets.KEY2 }}", + expected: map[string]string{ + "KEY1": "${{ secrets.KEY1 }}", + "KEY2": "${{ secrets.KEY2 }}", + }, + }, + { + name: "no secrets", + value: "plain value", + expected: map[string]string{}, + }, + { + name: "empty value", + value: "", + expected: map[string]string{}, + }, + { + name: "secret with complex default", + value: "${{ secrets.CONFIG || 'default-config-value' }}", + expected: map[string]string{ + "CONFIG": "${{ secrets.CONFIG || 'default-config-value' }}", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractSecretsFromValue(tt.value) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d secrets, got %d", len(tt.expected), len(result)) + } + + for varName, expr := range tt.expected { + if result[varName] != expr { + t.Errorf("Expected secret %q to have expression %q, got %q", varName, expr, result[varName]) + } + } + }) + } +} + +// TestSharedExtractSecretsFromMap tests the shared ExtractSecretsFromMap utility function +func TestSharedExtractSecretsFromMap(t *testing.T) { + tests := []struct { + name string + values map[string]string + expected map[string]string + }{ + { + name: "HTTP headers with secrets", + values: map[string]string{ + "DD_API_KEY": "${{ secrets.DD_API_KEY }}", + "DD_APPLICATION_KEY": "${{ secrets.DD_APPLICATION_KEY }}", + "DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}", + }, + expected: map[string]string{ + "DD_API_KEY": "${{ secrets.DD_API_KEY }}", + "DD_APPLICATION_KEY": "${{ secrets.DD_APPLICATION_KEY }}", + "DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}", + }, + }, + { + name: "env vars with secrets", + values: map[string]string{ + "API_KEY": "${{ secrets.API_KEY }}", + "TOKEN": "${{ secrets.TOKEN }}", + }, + expected: map[string]string{ + "API_KEY": "${{ secrets.API_KEY }}", + "TOKEN": "${{ secrets.TOKEN }}", + }, + }, + { + name: "mixed secrets and plain values", + values: map[string]string{ + "Authorization": "Bearer ${{ secrets.AUTH_TOKEN }}", + "Content-Type": "application/json", + "API_KEY": "${{ secrets.API_KEY }}", + }, + expected: map[string]string{ + "AUTH_TOKEN": "${{ secrets.AUTH_TOKEN }}", + "API_KEY": "${{ secrets.API_KEY }}", + }, + }, + { + name: "no secrets", + values: map[string]string{ + "SIMPLE_VAR": "plain value", + }, + expected: map[string]string{}, + }, + { + name: "duplicate secrets (same secret in multiple values)", + values: map[string]string{ + "Header1": "${{ secrets.API_KEY }}", + "Header2": "${{ secrets.API_KEY }}", + }, + expected: map[string]string{ + "API_KEY": "${{ secrets.API_KEY }}", + }, + }, + { + name: "empty map", + values: map[string]string{}, + expected: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractSecretsFromMap(tt.values) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d secrets, got %d", len(tt.expected), len(result)) + } + + for varName, expr := range tt.expected { + if result[varName] != expr { + t.Errorf("Expected secret %q to have expression %q, got %q", varName, expr, result[varName]) + } + } + }) + } +} + +// TestSharedReplaceSecretsWithEnvVars tests the shared ReplaceSecretsWithEnvVars utility function +func TestSharedReplaceSecretsWithEnvVars(t *testing.T) { + tests := []struct { + name string + value string + secrets map[string]string + expected string + }{ + { + name: "simple replacement", + value: "${{ secrets.DD_API_KEY }}", + secrets: map[string]string{ + "DD_API_KEY": "${{ secrets.DD_API_KEY }}", + }, + expected: "\\${DD_API_KEY}", + }, + { + name: "replacement with default value", + value: "${{ secrets.DD_SITE || 'datadoghq.com' }}", + secrets: map[string]string{ + "DD_SITE": "${{ secrets.DD_SITE || 'datadoghq.com' }}", + }, + expected: "\\${DD_SITE}", + }, + { + name: "bearer token replacement", + value: "Bearer ${{ secrets.TAVILY_API_KEY }}", + secrets: map[string]string{ + "TAVILY_API_KEY": "${{ secrets.TAVILY_API_KEY }}", + }, + expected: "Bearer \\${TAVILY_API_KEY}", + }, + { + name: "multiple replacements", + value: "${{ secrets.KEY1 }} and ${{ secrets.KEY2 }}", + secrets: map[string]string{ + "KEY1": "${{ secrets.KEY1 }}", + "KEY2": "${{ secrets.KEY2 }}", + }, + expected: "\\${KEY1} and \\${KEY2}", + }, + { + name: "no replacements", + value: "plain value", + secrets: map[string]string{}, + expected: "plain value", + }, + { + name: "partial replacement", + value: "${{ secrets.API_KEY }} and plain text", + secrets: map[string]string{ + "API_KEY": "${{ secrets.API_KEY }}", + }, + expected: "\\${API_KEY} and plain text", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ReplaceSecretsWithEnvVars(tt.value, tt.secrets) + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +// TestSharedExtractSecretsFromValueEdgeCases tests edge cases for the shared ExtractSecretsFromValue utility function +func TestSharedExtractSecretsFromValueEdgeCases(t *testing.T) { + tests := []struct { + name string + value string + expected map[string]string + }{ + { + name: "malformed expression - missing closing braces", + value: "${{ secrets.KEY", + expected: map[string]string{}, + }, + { + name: "malformed expression - missing opening braces", + value: "secrets.KEY }}", + expected: map[string]string{}, + }, + { + name: "incomplete expression", + value: "${{ secrets.", + expected: map[string]string{}, + }, + { + name: "secret name with trailing space before pipe", + value: "${{ secrets.KEY || 'default' }}", + expected: map[string]string{ + "KEY": "${{ secrets.KEY || 'default' }}", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractSecretsFromValue(tt.value) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d secrets, got %d", len(tt.expected), len(result)) + } + + for varName, expr := range tt.expected { + if result[varName] != expr { + t.Errorf("Expected secret %q to have expression %q, got %q", varName, expr, result[varName]) + } + } + }) + } +} From 9450c8b10b3b0b270cc03523e024cd4d3634f604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:09:50 +0000 Subject: [PATCH 3/3] Remove thin wrapper extractSecretName, use workflow.ExtractSecretName directly Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/secrets.go | 12 ++---------- pkg/cli/secrets_test.go | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/pkg/cli/secrets.go b/pkg/cli/secrets.go index 74f3c7871f..2d0403baca 100644 --- a/pkg/cli/secrets.go +++ b/pkg/cli/secrets.go @@ -59,14 +59,6 @@ func checkSecretExists(secretName string) (bool, error) { return false, nil } -// extractSecretName extracts the secret name from a GitHub Actions expression -// Examples: "${{ secrets.DD_API_KEY }}" -> "DD_API_KEY" -// -// "${{ secrets.DD_SITE || 'datadoghq.com' }}" -> "DD_SITE" -func extractSecretName(value string) string { - return workflow.ExtractSecretName(value) -} - // extractSecretsFromConfig extracts all required secrets from an MCP server config func extractSecretsFromConfig(config parser.MCPServerConfig) []SecretInfo { secretsLog.Printf("Extracting secrets from MCP config: command=%s", config.Command) @@ -75,7 +67,7 @@ func extractSecretsFromConfig(config parser.MCPServerConfig) []SecretInfo { // Extract from HTTP headers for key, value := range config.Headers { - secretName := extractSecretName(value) + secretName := workflow.ExtractSecretName(value) if secretName != "" && !seen[secretName] { secrets = append(secrets, SecretInfo{ Name: secretName, @@ -87,7 +79,7 @@ func extractSecretsFromConfig(config parser.MCPServerConfig) []SecretInfo { // Extract from environment variables for key, value := range config.Env { - secretName := extractSecretName(value) + secretName := workflow.ExtractSecretName(value) if secretName != "" && !seen[secretName] { secrets = append(secrets, SecretInfo{ Name: secretName, diff --git a/pkg/cli/secrets_test.go b/pkg/cli/secrets_test.go index 129b61d1ce..8cf82639a7 100644 --- a/pkg/cli/secrets_test.go +++ b/pkg/cli/secrets_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/githubnext/gh-aw/pkg/parser" + "github.com/githubnext/gh-aw/pkg/workflow" ) func TestExtractSecretName(t *testing.T) { @@ -47,7 +48,7 @@ func TestExtractSecretName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := extractSecretName(tt.value) + result := workflow.ExtractSecretName(tt.value) if result != tt.expected { t.Errorf("Expected %q, got %q", tt.expected, result) }