From 0419daf285f283661d2fb29210930da1af33a776 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 04:42:22 +0000 Subject: [PATCH 1/3] Initial plan From 67db83e40cb6b44d1b9219e458111f8465322039 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 04:58:26 +0000 Subject: [PATCH 2/3] Fix wfctl secrets commands to use config.LoadFromFile for imports support - Replace direct os.ReadFile+yaml.Unmarshal in loadSecretsConfig, loadWorkflowConfigForSecrets, and runSecretsDetect in secrets_detect.go with config.LoadFromFile so import directives are honored - Replace direct os.ReadFile+yaml.Unmarshal in runSecretsSetup in secrets_setup.go with config.LoadFromFile - Replace direct os.ReadFile+yaml.Unmarshal in parseSecretsConfig in infra_secrets.go with config.LoadFromFile - Remove now-unused gopkg.in/yaml.v3 imports from secrets_detect.go and secrets_setup.go - Add regression tests in secrets_imports_test.go covering imported entries, secretStores, defaultStore resolution, and validate behavior Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/29c5afe6-a4f0-4dce-9a30-b911e006cc4b Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- cmd/wfctl/infra_secrets.go | 17 +- cmd/wfctl/secrets_detect.go | 33 +--- cmd/wfctl/secrets_imports_test.go | 312 ++++++++++++++++++++++++++++++ cmd/wfctl/secrets_setup.go | 11 +- 4 files changed, 331 insertions(+), 42 deletions(-) create mode 100644 cmd/wfctl/secrets_imports_test.go diff --git a/cmd/wfctl/infra_secrets.go b/cmd/wfctl/infra_secrets.go index d071009d..76641ed2 100644 --- a/cmd/wfctl/infra_secrets.go +++ b/cmd/wfctl/infra_secrets.go @@ -19,20 +19,15 @@ type SecretsConfig = config.SecretsConfig type SecretGen = config.SecretGen type InfraConfig = config.InfraConfig -// parseSecretsConfig reads the "secrets:" top-level key from a YAML file. -// Returns nil, nil if the section is absent. +// parseSecretsConfig reads the "secrets:" top-level key from a YAML file, +// honoring any imports: directives so that imported secretStores and entries +// are visible to callers. Returns nil, nil if the section is absent after merging. func parseSecretsConfig(cfgFile string) (*SecretsConfig, error) { - data, err := os.ReadFile(cfgFile) + cfg, err := config.LoadFromFile(cfgFile) if err != nil { - return nil, fmt.Errorf("read %s: %w", cfgFile, err) - } - var parsed struct { - Secrets *SecretsConfig `yaml:"secrets"` - } - if err := yaml.Unmarshal(data, &parsed); err != nil { - return nil, fmt.Errorf("parse secrets config %s: %w", cfgFile, err) + return nil, fmt.Errorf("load config %s: %w", cfgFile, err) } - return parsed.Secrets, nil + return cfg.Secrets, nil } // parseInfraConfig reads the "infra:" top-level section from a YAML file. diff --git a/cmd/wfctl/secrets_detect.go b/cmd/wfctl/secrets_detect.go index 2e91679a..5956917c 100644 --- a/cmd/wfctl/secrets_detect.go +++ b/cmd/wfctl/secrets_detect.go @@ -13,7 +13,6 @@ import ( "github.com/GoCodeAlone/workflow/secrets" "github.com/mattn/go-isatty" "golang.org/x/term" - "gopkg.in/yaml.v3" ) // secretFieldPatterns are field name substrings that indicate a secret value. @@ -35,16 +34,12 @@ func runSecretsDetect(args []string) error { return err } - data, err := os.ReadFile(*configFile) + cfg, err := config.LoadFromFile(*configFile) if err != nil { - return fmt.Errorf("read config: %w", err) - } - var cfg config.WorkflowConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return fmt.Errorf("parse config: %w", err) + return fmt.Errorf("load config: %w", err) } - detected := detectSecrets(&cfg) + detected := detectSecrets(cfg) if len(detected) == 0 { fmt.Println("No secret-like values detected.") return nil @@ -336,23 +331,19 @@ func secretStateLabel(state SecretState) string { // loadWorkflowConfigForSecrets loads the full WorkflowConfig for secret operations. // Falls back to a default env-provider config if the file does not exist. func loadWorkflowConfigForSecrets(configFile string) (*config.WorkflowConfig, error) { - data, err := os.ReadFile(configFile) + cfg, err := config.LoadFromFile(configFile) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { return &config.WorkflowConfig{ //nolint:nilerr // gracefully fall back when file is absent Secrets: &config.SecretsConfig{Provider: "env"}, }, nil } - return nil, fmt.Errorf("read config: %w", err) - } - var cfg config.WorkflowConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("parse config: %w", err) + return nil, fmt.Errorf("load config: %w", err) } if cfg.Secrets == nil { cfg.Secrets = &config.SecretsConfig{Provider: "env"} } - return &cfg, nil + return cfg, nil } func runSecretsValidate(args []string) error { @@ -486,16 +477,12 @@ func runSecretsSync(args []string) error { // loadSecretsConfig reads a workflow config and returns its SecretsConfig. // Returns a default env-provider config if no secrets: section is defined. func loadSecretsConfig(configFile string) (*config.SecretsConfig, error) { - data, err := os.ReadFile(configFile) + cfg, err := config.LoadFromFile(configFile) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { return &config.SecretsConfig{Provider: "env"}, nil //nolint:nilerr // gracefully fall back when file is absent } - return nil, fmt.Errorf("read config %q: %w", configFile, err) - } - var cfg config.WorkflowConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("parse config: %w", err) + return nil, fmt.Errorf("load config %q: %w", configFile, err) } if cfg.Secrets == nil { return &config.SecretsConfig{Provider: "env"}, nil diff --git a/cmd/wfctl/secrets_imports_test.go b/cmd/wfctl/secrets_imports_test.go new file mode 100644 index 00000000..88aa6901 --- /dev/null +++ b/cmd/wfctl/secrets_imports_test.go @@ -0,0 +1,312 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestLoadSecretsConfig_HonorsImports verifies that loadSecretsConfig processes +// import directives, making secrets declared only in imported files visible. +func TestLoadSecretsConfig_HonorsImports(t *testing.T) { + dir := t.TempDir() + + shared := `secrets: + defaultStore: vault + entries: + - name: API_TOKEN + description: API authentication token + - name: DB_PASSWORD +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := loadSecretsConfig(filepath.Join(dir, "main.yaml")) + if err != nil { + t.Fatalf("loadSecretsConfig: %v", err) + } + if cfg == nil { + t.Fatal("expected non-nil SecretsConfig") + } + if cfg.DefaultStore != "vault" { + t.Errorf("defaultStore = %q, want %q", cfg.DefaultStore, "vault") + } + if len(cfg.Entries) != 2 { + t.Fatalf("expected 2 entries, got %d: %+v", len(cfg.Entries), cfg.Entries) + } + names := make([]string, len(cfg.Entries)) + for i, e := range cfg.Entries { + names[i] = e.Name + } + for _, want := range []string{"API_TOKEN", "DB_PASSWORD"} { + found := false + for _, n := range names { + if n == want { + found = true + break + } + } + if !found { + t.Errorf("entry %q not found in imported secrets; got %v", want, names) + } + } +} + +// TestLoadSecretsConfig_MainWinsOverImport verifies that when the same entry +// is declared in both main and imported files, the main file's definition wins. +func TestLoadSecretsConfig_MainWinsOverImport(t *testing.T) { + dir := t.TempDir() + + shared := `secrets: + defaultStore: imported-store + entries: + - name: SHARED_SECRET +` + main := `imports: + - shared.yaml +secrets: + defaultStore: main-store + entries: + - name: MAIN_SECRET +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := loadSecretsConfig(filepath.Join(dir, "main.yaml")) + if err != nil { + t.Fatalf("loadSecretsConfig: %v", err) + } + // Main file's defaultStore wins. + if cfg.DefaultStore != "main-store" { + t.Errorf("defaultStore = %q, want %q", cfg.DefaultStore, "main-store") + } + // Both entries visible. + names := make(map[string]bool) + for _, e := range cfg.Entries { + names[e.Name] = true + } + if !names["MAIN_SECRET"] { + t.Error("expected MAIN_SECRET in merged entries") + } + if !names["SHARED_SECRET"] { + t.Error("expected SHARED_SECRET from import in merged entries") + } +} + +// TestLoadWorkflowConfigForSecrets_HonorsImports verifies that +// loadWorkflowConfigForSecrets merges secretStores from imported files. +func TestLoadWorkflowConfigForSecrets_HonorsImports(t *testing.T) { + dir := t.TempDir() + + shared := `secretStores: + vault: + provider: vault + config: + address: https://vault.example.com +secrets: + defaultStore: vault + entries: + - name: API_TOKEN +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := loadWorkflowConfigForSecrets(filepath.Join(dir, "main.yaml")) + if err != nil { + t.Fatalf("loadWorkflowConfigForSecrets: %v", err) + } + if cfg.SecretStores == nil { + t.Fatal("expected SecretStores to be populated from import") + } + if _, ok := cfg.SecretStores["vault"]; !ok { + t.Errorf("expected 'vault' store in SecretStores, got %v", cfg.SecretStores) + } + if cfg.Secrets == nil || cfg.Secrets.DefaultStore != "vault" { + t.Errorf("expected defaultStore=vault from import, got %+v", cfg.Secrets) + } + if len(cfg.Secrets.Entries) == 0 { + t.Error("expected entries from import, got none") + } +} + +// TestParseSecretsConfig_HonorsImports verifies that parseSecretsConfig +// (used by infra commands) merges the secrets section from imported files. +func TestParseSecretsConfig_HonorsImports(t *testing.T) { + dir := t.TempDir() + + shared := `secrets: + provider: env + entries: + - name: IMPORTED_SECRET + generate: + - key: IMPORTED_KEY + type: random_hex + length: 32 +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := parseSecretsConfig(filepath.Join(dir, "main.yaml")) + if err != nil { + t.Fatalf("parseSecretsConfig: %v", err) + } + if cfg == nil { + t.Fatal("expected non-nil SecretsConfig from import") + } + if len(cfg.Entries) == 0 { + t.Error("expected entries from import, got none") + } + if cfg.Entries[0].Name != "IMPORTED_SECRET" { + t.Errorf("entry[0].Name = %q, want %q", cfg.Entries[0].Name, "IMPORTED_SECRET") + } + if len(cfg.Generate) == 0 { + t.Error("expected generate entries from import, got none") + } + if cfg.Generate[0].Key != "IMPORTED_KEY" { + t.Errorf("generate[0].Key = %q, want %q", cfg.Generate[0].Key, "IMPORTED_KEY") + } +} + +// TestSecretsValidate_HonorsImports verifies that runSecretsValidate sees +// secrets entries that come from imported files. +func TestSecretsValidate_HonorsImports(t *testing.T) { + dir := t.TempDir() + + // Pre-set the env var so the secret is "set" during validate. + t.Setenv("IMPORTED_VALIDATE_KEY", "test-value") + + shared := `secrets: + provider: env + entries: + - name: IMPORTED_VALIDATE_KEY +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + err := runSecretsValidate([]string{"--config", filepath.Join(dir, "main.yaml")}) + if err != nil { + t.Errorf("runSecretsValidate: expected success with set secret from import, got: %v", err) + } +} + +// TestSecretsValidate_ImportedEntryUnset verifies that runSecretsValidate +// reports missing imported entries as errors. +func TestSecretsValidate_ImportedEntryUnset(t *testing.T) { + dir := t.TempDir() + + const envKey = "WFCTL_IMPORT_TEST_UNSET_KEY_XYZ123" + os.Unsetenv(envKey) + + shared := `secrets: + provider: env + entries: + - name: ` + envKey + ` +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + err := runSecretsValidate([]string{"--config", filepath.Join(dir, "main.yaml")}) + if err == nil { + t.Error("expected error for unset imported secret, got nil") + } + if !strings.Contains(err.Error(), envKey) { + t.Errorf("expected error to mention %q, got: %v", envKey, err) + } +} + +// TestSecretsSetup_HonorsImportedDefaultStore verifies that resolveSecretStoreForSetup +// uses the defaultStore from an imported secrets section. +func TestSecretsSetup_HonorsImportedDefaultStore(t *testing.T) { + dir := t.TempDir() + + shared := `secrets: + defaultStore: vault + entries: + - name: MY_SECRET +` + main := `imports: + - shared.yaml +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.yaml"), []byte(main), 0o600); err != nil { + t.Fatal(err) + } + + wfCfg, err := loadWorkflowConfigForSecrets(filepath.Join(dir, "main.yaml")) + if err != nil { + t.Fatalf("loadWorkflowConfigForSecrets: %v", err) + } + + // Find the MY_SECRET entry in the merged config. + var entry SecretsConfig + if wfCfg.Secrets != nil { + entry = *wfCfg.Secrets + } + _ = entry + + if len(wfCfg.Secrets.Entries) == 0 { + t.Fatal("expected entries from import, got none") + } + secretEntry := wfCfg.Secrets.Entries[0] + store := resolveSecretStoreForSetup(secretEntry, "local", wfCfg) + if store != "vault" { + t.Errorf("resolveSecretStoreForSetup = %q, want %q (from imported defaultStore)", store, "vault") + } +} + +// TestLoadSecretsConfig_MissingFile verifies that a missing config file +// returns a default env-provider config rather than an error. +func TestLoadSecretsConfig_MissingFile_DefaultsToEnv(t *testing.T) { + cfg, err := loadSecretsConfig(filepath.Join(t.TempDir(), "nonexistent.yaml")) + if err != nil { + t.Errorf("expected no error for missing file, got: %v", err) + } + if cfg == nil { + t.Fatal("expected default config, got nil") + } + if cfg.Provider != "env" { + t.Errorf("expected default provider 'env', got %q", cfg.Provider) + } +} diff --git a/cmd/wfctl/secrets_setup.go b/cmd/wfctl/secrets_setup.go index d759ce94..b2217d51 100644 --- a/cmd/wfctl/secrets_setup.go +++ b/cmd/wfctl/secrets_setup.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/GoCodeAlone/workflow/config" - "gopkg.in/yaml.v3" ) // runSecretsSetup implements `wfctl secrets setup --env `. @@ -35,13 +34,9 @@ Options: return err } - data, err := os.ReadFile(*configFile) + cfg, err := config.LoadFromFile(*configFile) if err != nil { - return fmt.Errorf("read config: %w", err) - } - var cfg config.WorkflowConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return fmt.Errorf("parse config: %w", err) + return fmt.Errorf("load config: %w", err) } if cfg.Secrets == nil || len(cfg.Secrets.Entries) == 0 { @@ -56,7 +51,7 @@ Options: var set, skipped int for _, entry := range cfg.Secrets.Entries { - storeName := resolveSecretStoreForSetup(entry, *envName, &cfg) + storeName := resolveSecretStoreForSetup(entry, *envName, cfg) provider, provErr := newSecretsProvider(storeName) if provErr != nil { fmt.Printf(" %-24s [SKIP] store %q not accessible: %v\n", entry.Name, storeName, provErr) From 91434188f57eb17383761576f5d8d06ebdd45119 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 06:25:24 +0000 Subject: [PATCH 3/3] Address review feedback on secrets setup and test improvements - Replace resolveSecretStoreForSetup+newSecretsProvider in runSecretsSetup with ResolveSecretStore+getProviderForStore to correctly use SecretsStoreOverride (not SecretsProvider) for env-level overrides and to properly look up named stores from SecretStores map - Remove the now-redundant resolveSecretStoreForSetup function - Fix parseSecretsConfig comment: remove misleading mention of secretStores (the function only returns cfg.Secrets, not SecretStores) - Improve TestLoadSecretsConfig_MainWinsOverImport: add a duplicate entry name in both files to actually exercise conflict-resolution (main wins) and assert no duplicates in the merged result - Update TestSecretsSetup_HonorsImportedDefaultStore: remove leftover dead scaffolding (entry/_ = entry) and use ResolveSecretStore directly Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/570bc092-953b-4c28-873c-ba02086b0ad6 Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- cmd/wfctl/infra_secrets.go | 5 ++-- cmd/wfctl/secrets_imports_test.go | 50 +++++++++++++++++++++---------- cmd/wfctl/secrets_setup.go | 29 ++---------------- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/cmd/wfctl/infra_secrets.go b/cmd/wfctl/infra_secrets.go index 76641ed2..eb8d50ff 100644 --- a/cmd/wfctl/infra_secrets.go +++ b/cmd/wfctl/infra_secrets.go @@ -20,8 +20,9 @@ type SecretGen = config.SecretGen type InfraConfig = config.InfraConfig // parseSecretsConfig reads the "secrets:" top-level key from a YAML file, -// honoring any imports: directives so that imported secretStores and entries -// are visible to callers. Returns nil, nil if the section is absent after merging. +// honoring any imports: directives so that the merged secrets section +// (entries, defaultStore, generate, etc.) is visible to callers. +// Returns nil, nil if the section is absent after merging. func parseSecretsConfig(cfgFile string) (*SecretsConfig, error) { cfg, err := config.LoadFromFile(cfgFile) if err != nil { diff --git a/cmd/wfctl/secrets_imports_test.go b/cmd/wfctl/secrets_imports_test.go index 88aa6901..79358e0c 100644 --- a/cmd/wfctl/secrets_imports_test.go +++ b/cmd/wfctl/secrets_imports_test.go @@ -61,7 +61,9 @@ func TestLoadSecretsConfig_HonorsImports(t *testing.T) { } // TestLoadSecretsConfig_MainWinsOverImport verifies that when the same entry -// is declared in both main and imported files, the main file's definition wins. +// name is declared in both main and imported files, the main file's definition +// wins (first-definition-wins semantics), and that unique entries from both +// files are present in the merged result. func TestLoadSecretsConfig_MainWinsOverImport(t *testing.T) { dir := t.TempDir() @@ -69,6 +71,9 @@ func TestLoadSecretsConfig_MainWinsOverImport(t *testing.T) { defaultStore: imported-store entries: - name: SHARED_SECRET + description: from-import + - name: DUPLICATE_SECRET + description: imported-desc ` main := `imports: - shared.yaml @@ -76,6 +81,8 @@ secrets: defaultStore: main-store entries: - name: MAIN_SECRET + - name: DUPLICATE_SECRET + description: main-desc ` if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(shared), 0o600); err != nil { t.Fatal(err) @@ -92,17 +99,35 @@ secrets: if cfg.DefaultStore != "main-store" { t.Errorf("defaultStore = %q, want %q", cfg.DefaultStore, "main-store") } - // Both entries visible. - names := make(map[string]bool) + // Collect entries by name. + byName := make(map[string]string) // name → description for _, e := range cfg.Entries { - names[e.Name] = true + byName[e.Name] = e.Description } - if !names["MAIN_SECRET"] { + // Main-only entry present. + if _, ok := byName["MAIN_SECRET"]; !ok { t.Error("expected MAIN_SECRET in merged entries") } - if !names["SHARED_SECRET"] { + // Import-only entry present. + if _, ok := byName["SHARED_SECRET"]; !ok { t.Error("expected SHARED_SECRET from import in merged entries") } + // Duplicate entry: main definition wins (first-definition-wins). + if desc, ok := byName["DUPLICATE_SECRET"]; !ok { + t.Error("expected DUPLICATE_SECRET in merged entries") + } else if desc != "main-desc" { + t.Errorf("DUPLICATE_SECRET.description = %q, want %q (main wins)", desc, "main-desc") + } + // No duplicate entries. + seen := make(map[string]int) + for _, e := range cfg.Entries { + seen[e.Name]++ + } + for name, count := range seen { + if count > 1 { + t.Errorf("entry %q appears %d times; expected exactly 1", name, count) + } + } } // TestLoadWorkflowConfigForSecrets_HonorsImports verifies that @@ -254,7 +279,7 @@ func TestSecretsValidate_ImportedEntryUnset(t *testing.T) { } } -// TestSecretsSetup_HonorsImportedDefaultStore verifies that resolveSecretStoreForSetup +// TestSecretsSetup_HonorsImportedDefaultStore verifies that ResolveSecretStore // uses the defaultStore from an imported secrets section. func TestSecretsSetup_HonorsImportedDefaultStore(t *testing.T) { dir := t.TempDir() @@ -279,20 +304,13 @@ func TestSecretsSetup_HonorsImportedDefaultStore(t *testing.T) { t.Fatalf("loadWorkflowConfigForSecrets: %v", err) } - // Find the MY_SECRET entry in the merged config. - var entry SecretsConfig - if wfCfg.Secrets != nil { - entry = *wfCfg.Secrets - } - _ = entry - if len(wfCfg.Secrets.Entries) == 0 { t.Fatal("expected entries from import, got none") } secretEntry := wfCfg.Secrets.Entries[0] - store := resolveSecretStoreForSetup(secretEntry, "local", wfCfg) + store := ResolveSecretStore(secretEntry.Name, "local", wfCfg) if store != "vault" { - t.Errorf("resolveSecretStoreForSetup = %q, want %q (from imported defaultStore)", store, "vault") + t.Errorf("ResolveSecretStore = %q, want %q (from imported defaultStore)", store, "vault") } } diff --git a/cmd/wfctl/secrets_setup.go b/cmd/wfctl/secrets_setup.go index b2217d51..ab7c8783 100644 --- a/cmd/wfctl/secrets_setup.go +++ b/cmd/wfctl/secrets_setup.go @@ -51,8 +51,8 @@ Options: var set, skipped int for _, entry := range cfg.Secrets.Entries { - storeName := resolveSecretStoreForSetup(entry, *envName, cfg) - provider, provErr := newSecretsProvider(storeName) + storeName := ResolveSecretStore(entry.Name, *envName, cfg) + provider, provErr := getProviderForStore(storeName, cfg) if provErr != nil { fmt.Printf(" %-24s [SKIP] store %q not accessible: %v\n", entry.Name, storeName, provErr) skipped++ @@ -118,31 +118,6 @@ Options: return nil } -// resolveSecretStoreForSetup determines which store to use for a secret in a given environment. -// Priority: per-secret store field → environment override → defaultStore → legacy provider → "env". -// This matches the order in ResolveSecretStore so that setup and runtime agree on which store owns a secret. -func resolveSecretStoreForSetup(entry config.SecretEntry, envName string, cfg *config.WorkflowConfig) string { - // 1. Per-secret store field (highest priority). - if entry.Store != "" { - return entry.Store - } - // 2. Environment-level store override. - if cfg.Environments != nil { - if env, ok := cfg.Environments[envName]; ok && env.SecretsProvider != "" { - return env.SecretsProvider - } - } - // 3. Default store from secretStores config. - if cfg.Secrets != nil && cfg.Secrets.DefaultStore != "" { - return cfg.Secrets.DefaultStore - } - // 4. Legacy provider field. - if cfg.Secrets != nil && cfg.Secrets.Provider != "" { - return cfg.Secrets.Provider - } - return "env" -} - // isAutoGenCandidate returns true if the secret name looks like a key, token, or signing secret. func isAutoGenCandidate(name string) bool { upper := strings.ToUpper(name)