| | | 1 | | package config |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "fmt" |
| | | 5 | | "strings" |
| | | 6 | | |
| | | 7 | | "github.com/go-viper/mapstructure/v2" |
| | | 8 | | "github.com/spf13/viper" |
| | | 9 | | |
| | | 10 | | "github.com/jedi-knights/go-semantic-release/internal/domain" |
| | | 11 | | "github.com/jedi-knights/go-semantic-release/internal/ports" |
| | | 12 | | ) |
| | | 13 | | |
| | | 14 | | // Compile-time interface compliance check. |
| | | 15 | | var _ ports.ConfigProvider = (*ViperProvider)(nil) |
| | | 16 | | |
| | | 17 | | // Supported config file names, searched in order (matching semantic-release conventions). |
| | | 18 | | var configNames = []string{ |
| | | 19 | | ".semantic-release", |
| | | 20 | | ".releaserc", |
| | | 21 | | "release.config", |
| | | 22 | | } |
| | | 23 | | |
| | | 24 | | // ViperProvider implements ports.ConfigProvider using Viper. |
| | | 25 | | type ViperProvider struct{} |
| | | 26 | | |
| | | 27 | | // NewViperProvider creates a new Viper-based config provider. |
| | | 28 | | func NewViperProvider() *ViperProvider { |
| | | 29 | | return &ViperProvider{} |
| | | 30 | | } |
| | | 31 | | |
| | 7 | 32 | | func (p *ViperProvider) Load(path string) (domain.Config, error) { |
| | 7 | 33 | | cfg := domain.DefaultConfig() |
| | 7 | 34 | | |
| | 7 | 35 | | v := viper.New() |
| | 7 | 36 | | v.SetEnvPrefix("SEMANTIC_RELEASE") |
| | 7 | 37 | | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) |
| | 7 | 38 | | v.AutomaticEnv() |
| | 7 | 39 | | |
| | 6 | 40 | | if path != "" { |
| | 6 | 41 | | v.SetConfigFile(path) |
| | 1 | 42 | | } else { |
| | 1 | 43 | | // Search for multiple config file names/formats. |
| | 1 | 44 | | v.AddConfigPath(".") |
| | 1 | 45 | | found := false |
| | 1 | 46 | | for _, name := range configNames { |
| | 3 | 47 | | v.SetConfigName(name) |
| | 0 | 48 | | if err := v.ReadInConfig(); err == nil { |
| | 0 | 49 | | found = true |
| | 0 | 50 | | break |
| | | 51 | | } |
| | | 52 | | } |
| | 1 | 53 | | if !found { |
| | 1 | 54 | | // No config file found — use defaults + env only. |
| | 1 | 55 | | return cfg, nil |
| | 1 | 56 | | } |
| | | 57 | | } |
| | | 58 | | |
| | 6 | 59 | | if path != "" { |
| | 2 | 60 | | if err := v.ReadInConfig(); err != nil { |
| | 2 | 61 | | return cfg, fmt.Errorf("reading config: %w", err) |
| | 2 | 62 | | } |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | // StringToGitHubAssetHookFunc must run first so string values are promoted to |
| | | 66 | | // GitHubAsset{Path: s} before mapstructure attempts its own map→struct decode. |
| | 4 | 67 | | if err := v.Unmarshal(&cfg, viper.DecodeHook( |
| | 4 | 68 | | mapstructure.ComposeDecodeHookFunc( |
| | 4 | 69 | | StringToGitHubAssetHookFunc(), |
| | 4 | 70 | | mapstructure.StringToTimeDurationHookFunc(), |
| | 4 | 71 | | mapstructure.StringToSliceHookFunc(","), |
| | 4 | 72 | | ), |
| | 0 | 73 | | )); err != nil { |
| | 0 | 74 | | return cfg, fmt.Errorf("unmarshaling config: %w", err) |
| | 0 | 75 | | } |
| | | 76 | | |
| | | 77 | | // Resolve extended configurations. |
| | 0 | 78 | | if len(cfg.Extends) > 0 { |
| | 0 | 79 | | resolved, err := ResolveExtends(cfg) |
| | 0 | 80 | | if err != nil { |
| | 0 | 81 | | return cfg, fmt.Errorf("resolving extends: %w", err) |
| | 0 | 82 | | } |
| | 0 | 83 | | cfg = resolved |
| | | 84 | | } |
| | | 85 | | |
| | 4 | 86 | | return cfg, nil |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | // WriteDefaultConfig writes a default config file to the given path. |
| | | 90 | | func WriteDefaultConfig(path string) error { |
| | | 91 | | v := viper.New() |
| | | 92 | | v.SetConfigType("yaml") |
| | | 93 | | |
| | | 94 | | v.Set("release_mode", "repo") |
| | | 95 | | v.Set("tag_format", "v{{.Version}}") |
| | | 96 | | v.Set("project_tag_format", "{{.Project}}/v{{.Version}}") |
| | | 97 | | v.Set("dry_run", false) |
| | | 98 | | v.Set("ci", true) |
| | | 99 | | v.Set("discover_modules", false) |
| | | 100 | | v.Set("dependency_propagation", false) |
| | | 101 | | v.Set("github.create_release", true) |
| | | 102 | | |
| | | 103 | | if err := v.WriteConfigAs(path); err != nil { |
| | | 104 | | return fmt.Errorf("writing default config to %s: %w", path, err) |
| | | 105 | | } |
| | | 106 | | return nil |
| | | 107 | | } |