From f130bfa1140ac8278412880078edc61313a2339d 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:36 +0000 Subject: [PATCH 1/2] Initial plan From 200626753fbed54fd60b1b4e1de2c96a9d3b4e70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 06:10:05 +0000 Subject: [PATCH 2/2] fix: parseSecretsConfig/parseInfraConfig now honor imports via config.LoadFromFile Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/156113c0-7bf1-490b-aedc-85a00360eec7 Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- cmd/wfctl/infra_secrets.go | 26 +++------- cmd/wfctl/infra_secrets_test.go | 86 +++++++++++++++++++++++++++++++++ config/config.go | 4 ++ config/config_import_test.go | 40 +++++++++++++++ 4 files changed, 138 insertions(+), 18 deletions(-) diff --git a/cmd/wfctl/infra_secrets.go b/cmd/wfctl/infra_secrets.go index d071009d..163fd766 100644 --- a/cmd/wfctl/infra_secrets.go +++ b/cmd/wfctl/infra_secrets.go @@ -2,11 +2,9 @@ package main import ( "fmt" - "os" "github.com/GoCodeAlone/workflow/config" "github.com/GoCodeAlone/workflow/secrets" - "gopkg.in/yaml.v3" ) // SecretsConfig, SecretGen and InfraConfig are type aliases for the canonical @@ -20,35 +18,27 @@ type SecretGen = config.SecretGen type InfraConfig = config.InfraConfig // parseSecretsConfig reads the "secrets:" top-level key from a YAML file. +// Imports declared in the file are resolved via config.LoadFromFile so that +// secrets.generate / secrets.entries blocks in imported files are visible. // Returns nil, nil if the section is absent. 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 parsed.Secrets, nil + return cfg.Secrets, nil } // parseInfraConfig reads the "infra:" top-level section from a YAML file. +// Imports declared in the file are resolved via config.LoadFromFile so that +// infra: blocks in imported files are visible. // Returns nil, nil if the section is absent. func parseInfraConfig(cfgFile string) (*InfraConfig, 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 { - Infra *InfraConfig `yaml:"infra"` - } - if err := yaml.Unmarshal(data, &parsed); err != nil { return nil, fmt.Errorf("parse infra config %s: %w", cfgFile, err) } - return parsed.Infra, nil + return cfg.Infra, nil } // resolveSecretsProvider constructs the appropriate secrets.Provider from cfg. diff --git a/cmd/wfctl/infra_secrets_test.go b/cmd/wfctl/infra_secrets_test.go index 1d8d2772..25d7808d 100644 --- a/cmd/wfctl/infra_secrets_test.go +++ b/cmd/wfctl/infra_secrets_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "path/filepath" "testing" @@ -182,3 +183,88 @@ func TestResolveSecretsProvider_KeychainMissingService(t *testing.T) { // Ensure GitHubSecretsProvider satisfies secrets.Provider interface. var _ secrets.Provider = (*secrets.GitHubSecretsProvider)(nil) + +// TestParseSecretsConfig_HonorsImports verifies that parseSecretsConfig +// resolves imports: a secrets.generate block declared only in an imported file +// is visible in the returned SecretsConfig. +func TestParseSecretsConfig_HonorsImports(t *testing.T) { + dir := t.TempDir() + + importedYAML := ` +secrets: + provider: env + config: + prefix: TEST_ + generate: + - key: STAGING_PG_PASSWORD + type: random_hex + length: 32 +` + mainYAML := ` +imports: + - shared.yaml + +modules: + - name: dummy + type: noop +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0o600); err != nil { + t.Fatal(err) + } + mainPath := filepath.Join(dir, "main.yaml") + if err := os.WriteFile(mainPath, []byte(mainYAML), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := parseSecretsConfig(mainPath) + if err != nil { + t.Fatalf("parseSecretsConfig: %v", err) + } + if cfg == nil { + t.Fatal("expected non-nil SecretsConfig from imported file, got nil") + } + if len(cfg.Generate) != 1 { + t.Fatalf("expected 1 generate entry from import, got %d", len(cfg.Generate)) + } + if cfg.Generate[0].Key != "STAGING_PG_PASSWORD" { + t.Errorf("expected key STAGING_PG_PASSWORD, got %q", cfg.Generate[0].Key) + } +} + +// TestParseInfraConfig_HonorsImports verifies that parseInfraConfig resolves +// imports: an infra: block declared only in an imported file is visible in the +// returned InfraConfig. +func TestParseInfraConfig_HonorsImports(t *testing.T) { + dir := t.TempDir() + + importedYAML := ` +infra: + auto_bootstrap: false +` + mainYAML := ` +imports: + - shared.yaml + +modules: + - name: dummy + type: noop +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0o600); err != nil { + t.Fatal(err) + } + mainPath := filepath.Join(dir, "main.yaml") + if err := os.WriteFile(mainPath, []byte(mainYAML), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := parseInfraConfig(mainPath) + if err != nil { + t.Fatalf("parseInfraConfig: %v", err) + } + if cfg == nil { + t.Fatal("expected non-nil InfraConfig from imported file, got nil") + } + if cfg.AutoBootstrap == nil || *cfg.AutoBootstrap { + t.Error("expected auto_bootstrap=false from import") + } +} diff --git a/config/config.go b/config/config.go index f2b92d88..df369a3b 100644 --- a/config/config.go +++ b/config/config.go @@ -452,6 +452,10 @@ func (cfg *WorkflowConfig) processImports(seen map[string]bool) error { existingEntries[e.Name] = struct{}{} } } + // Merge Infra config — parent wins; import fills in if parent has no Infra block. + if impCfg.Infra != nil && cfg.Infra == nil { + cfg.Infra = impCfg.Infra + } } cfg.Imports = nil // clear after processing diff --git a/config/config_import_test.go b/config/config_import_test.go index e83d59b6..2ed8a057 100644 --- a/config/config_import_test.go +++ b/config/config_import_test.go @@ -970,3 +970,43 @@ environments: t.Error("expected Environments[local] from main") } } + +// TestProcessImports_MergesInfraFromImport pins that WorkflowConfig.Infra is +// merged from imported files when the main config has no infra: block. +// parseInfraConfig (cmd/wfctl) uses config.LoadFromFile, so this exercises +// the same code path as wfctl infra apply auto-bootstrap detection. +func TestProcessImports_MergesInfraFromImport(t *testing.T) { + dir := t.TempDir() + + importedYAML := ` +infra: + auto_bootstrap: false +` + if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0644); err != nil { + t.Fatal(err) + } + + mainYAML := ` +imports: + - shared.yaml + +modules: + - name: dummy + type: noop +` + mainPath := filepath.Join(dir, "main.yaml") + if err := os.WriteFile(mainPath, []byte(mainYAML), 0644); err != nil { + t.Fatal(err) + } + + cfg, err := LoadFromFile(mainPath) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Infra == nil { + t.Fatal("expected cfg.Infra to be populated from import, got nil") + } + if cfg.Infra.AutoBootstrap == nil || *cfg.Infra.AutoBootstrap { + t.Error("expected AutoBootstrap=false from import") + } +}