Centralized Configuration Service Implementation (#319)
* feat: implement centralized configuration service using viper This commit introduces a centralized configuration service in `pkg/config` to reduce code duplication and provide a unified way to manage configuration across the project. Key changes: - Refactored `pkg/config` to use `github.com/spf13/viper` as the backend. - Implemented `core.Config` interface with support for layered resolution (defaults, files, environment variables). - Added `LoadFile` to support merging multiple configuration files, with automatic type detection for YAML and .env files. - Migrated `pkg/agentic`, `pkg/devops`, `pkg/build`, and `pkg/release` to use the new centralized service. - Added `mapstructure` tags to configuration structs to support viper unmarshaling. - Added comprehensive tests for the new configuration service features. This addresses the recommendations from the Architecture & Design Pattern Audit. * feat: implement centralized configuration service and address security alerts - Introduced centralized configuration service in `pkg/config` using `viper`. - Migrated major packages (`agentic`, `devops`, `build`, `release`) to the new service. - Resolved merge conflicts with `dev` branch. - Addressed CodeQL security alert by making UniFi TLS verification configurable. - Fixed `go.mod` to ensure it is tidy and consistent with direct dependencies. - Updated UniFi CLI to support TLS verification configuration.
This commit is contained in:
parent
38db43bbfb
commit
a169558102
10 changed files with 323 additions and 335 deletions
12
go.mod
12
go.mod
|
|
@ -16,7 +16,9 @@ require (
|
|||
github.com/ollama/ollama v0.15.4
|
||||
github.com/qdrant/go-client v1.16.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/unpoller/unifi/v5 v5.17.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/mod v0.32.0
|
||||
golang.org/x/net v0.49.0
|
||||
|
|
@ -48,6 +50,7 @@ require (
|
|||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
|
|
@ -55,6 +58,7 @@ require (
|
|||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
|
|
@ -80,24 +84,29 @@ require (
|
|||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.18.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/unpoller/unifi/v5 v5.17.0 // indirect
|
||||
github.com/wI2L/jsondiff v0.7.0 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64 // indirect
|
||||
|
|
@ -106,6 +115,7 @@ require (
|
|||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -57,6 +57,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
|||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
|
|
@ -87,6 +89,8 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6
|
|||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
|
|
@ -180,6 +184,8 @@ github.com/ollama/ollama v0.15.4 h1:y841GH5lsi5j5BTFyX/E+UOC3Yiw+JBfdjBVRGw+I0M=
|
|||
github.com/ollama/ollama v0.15.4/go.mod h1:4Yn3jw2hZ4VqyJ1XciYawDRE8bzv4RT3JiVZR1kCfwE=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
|
|
@ -199,6 +205,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
|
|
@ -210,17 +218,27 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
|||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/config"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/host-uk/core/pkg/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds the configuration for connecting to the core-agentic service.
|
||||
type Config struct {
|
||||
// BaseURL is the URL of the core-agentic API server.
|
||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||
BaseURL string `yaml:"base_url" json:"base_url" mapstructure:"base_url"`
|
||||
// Token is the authentication token for API requests.
|
||||
Token string `yaml:"token" json:"token"`
|
||||
Token string `yaml:"token" json:"token" mapstructure:"token"`
|
||||
// DefaultProject is the project to use when none is specified.
|
||||
DefaultProject string `yaml:"default_project" json:"default_project"`
|
||||
DefaultProject string `yaml:"default_project" json:"default_project" mapstructure:"default_project"`
|
||||
// AgentID is the identifier for this agent (optional, used for claiming tasks).
|
||||
AgentID string `yaml:"agent_id" json:"agent_id"`
|
||||
AgentID string `yaml:"agent_id" json:"agent_id" mapstructure:"agent_id"`
|
||||
}
|
||||
|
||||
// configFileName is the name of the YAML config file.
|
||||
|
|
@ -32,10 +33,9 @@ const envFileName = ".env"
|
|||
const DefaultBaseURL = "https://api.core-agentic.dev"
|
||||
|
||||
// LoadConfig loads the agentic configuration from the specified directory.
|
||||
// It first checks for a .env file, then falls back to ~/.core/agentic.yaml.
|
||||
// If dir is empty, it checks the current directory first.
|
||||
// It uses the centralized config service.
|
||||
//
|
||||
// Environment variables take precedence:
|
||||
// Environment variables take precedence (prefix: AGENTIC_):
|
||||
// - AGENTIC_BASE_URL: API base URL
|
||||
// - AGENTIC_TOKEN: Authentication token
|
||||
// - AGENTIC_PROJECT: Default project
|
||||
|
|
@ -58,7 +58,6 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
|
||||
// Try loading from current directory .env
|
||||
if dir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
envPath := filepath.Join(cwd, envFileName)
|
||||
|
|
@ -69,7 +68,6 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try loading from ~/.core/agentic.yaml
|
||||
homeDir, err := os.UserHomeDir()
|
||||
|
|
@ -78,9 +76,16 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
|
||||
configPath := filepath.Join(homeDir, ".core", configFileName)
|
||||
if err := loadYAMLConfig(configPath, cfg); err != nil && !os.IsNotExist(err) {
|
||||
if io.Local.IsFile(configPath) {
|
||||
// Use centralized config service to load the YAML file
|
||||
c, err := config.New(config.WithPath(configPath))
|
||||
if err != nil {
|
||||
return nil, log.E("agentic.LoadConfig", "failed to initialize config", err)
|
||||
}
|
||||
if err := c.Get("", cfg); err != nil {
|
||||
return nil, log.E("agentic.LoadConfig", "failed to load config", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply environment variable overrides
|
||||
applyEnvOverrides(cfg)
|
||||
|
|
@ -137,16 +142,6 @@ func loadEnvFile(path string, cfg *Config) error {
|
|||
return scanner.Err()
|
||||
}
|
||||
|
||||
// loadYAMLConfig reads configuration from a YAML file.
|
||||
func loadYAMLConfig(path string, cfg *Config) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return yaml.Unmarshal(data, cfg)
|
||||
}
|
||||
|
||||
// applyEnvOverrides applies environment variable overrides to the config.
|
||||
func applyEnvOverrides(cfg *Config) {
|
||||
if v := os.Getenv("AGENTIC_BASE_URL"); v != "" {
|
||||
|
|
@ -165,28 +160,18 @@ func applyEnvOverrides(cfg *Config) {
|
|||
|
||||
// SaveConfig saves the configuration to ~/.core/agentic.yaml.
|
||||
func SaveConfig(cfg *Config) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
path, err := ConfigPath()
|
||||
if err != nil {
|
||||
return log.E("agentic.SaveConfig", "failed to get home directory", err)
|
||||
return err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".core")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return log.E("agentic.SaveConfig", "failed to create config directory", err)
|
||||
}
|
||||
data := make(map[string]any)
|
||||
data["base_url"] = cfg.BaseURL
|
||||
data["token"] = cfg.Token
|
||||
data["default_project"] = cfg.DefaultProject
|
||||
data["agent_id"] = cfg.AgentID
|
||||
|
||||
configPath := filepath.Join(configDir, configFileName)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return log.E("agentic.SaveConfig", "failed to marshal config", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
return log.E("agentic.SaveConfig", "failed to write config file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return config.Save(io.Local, path, data)
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the config file in the user's home directory.
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ package build
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/build/signing"
|
||||
"github.com/host-uk/core/pkg/config"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ConfigFileName is the name of the build configuration file.
|
||||
|
|
@ -22,48 +21,48 @@ const ConfigDir = ".core"
|
|||
// This is distinct from Config which holds runtime build parameters.
|
||||
type BuildConfig struct {
|
||||
// Version is the config file format version.
|
||||
Version int `yaml:"version"`
|
||||
Version int `yaml:"version" mapstructure:"version"`
|
||||
// Project contains project metadata.
|
||||
Project Project `yaml:"project"`
|
||||
Project Project `yaml:"project" mapstructure:"project"`
|
||||
// Build contains build settings.
|
||||
Build Build `yaml:"build"`
|
||||
Build Build `yaml:"build" mapstructure:"build"`
|
||||
// Targets defines the build targets.
|
||||
Targets []TargetConfig `yaml:"targets"`
|
||||
Targets []TargetConfig `yaml:"targets" mapstructure:"targets"`
|
||||
// Sign contains code signing configuration.
|
||||
Sign signing.SignConfig `yaml:"sign,omitempty"`
|
||||
Sign signing.SignConfig `yaml:"sign,omitempty" mapstructure:"sign,omitempty"`
|
||||
}
|
||||
|
||||
// Project holds project metadata.
|
||||
type Project struct {
|
||||
// Name is the project name.
|
||||
Name string `yaml:"name"`
|
||||
Name string `yaml:"name" mapstructure:"name"`
|
||||
// Description is a brief description of the project.
|
||||
Description string `yaml:"description"`
|
||||
Description string `yaml:"description" mapstructure:"description"`
|
||||
// Main is the path to the main package (e.g., ./cmd/core).
|
||||
Main string `yaml:"main"`
|
||||
Main string `yaml:"main" mapstructure:"main"`
|
||||
// Binary is the output binary name.
|
||||
Binary string `yaml:"binary"`
|
||||
Binary string `yaml:"binary" mapstructure:"binary"`
|
||||
}
|
||||
|
||||
// Build holds build-time settings.
|
||||
type Build struct {
|
||||
// CGO enables CGO for the build.
|
||||
CGO bool `yaml:"cgo"`
|
||||
CGO bool `yaml:"cgo" mapstructure:"cgo"`
|
||||
// Flags are additional build flags (e.g., ["-trimpath"]).
|
||||
Flags []string `yaml:"flags"`
|
||||
Flags []string `yaml:"flags" mapstructure:"flags"`
|
||||
// LDFlags are linker flags (e.g., ["-s", "-w"]).
|
||||
LDFlags []string `yaml:"ldflags"`
|
||||
LDFlags []string `yaml:"ldflags" mapstructure:"ldflags"`
|
||||
// Env are additional environment variables.
|
||||
Env []string `yaml:"env"`
|
||||
Env []string `yaml:"env" mapstructure:"env"`
|
||||
}
|
||||
|
||||
// TargetConfig defines a build target in the config file.
|
||||
// This is separate from Target to allow for additional config-specific fields.
|
||||
type TargetConfig struct {
|
||||
// OS is the target operating system (e.g., "linux", "darwin", "windows").
|
||||
OS string `yaml:"os"`
|
||||
OS string `yaml:"os" mapstructure:"os"`
|
||||
// Arch is the target architecture (e.g., "amd64", "arm64").
|
||||
Arch string `yaml:"arch"`
|
||||
Arch string `yaml:"arch" mapstructure:"arch"`
|
||||
}
|
||||
|
||||
// LoadConfig loads build configuration from the .core/build.yaml file in the given directory.
|
||||
|
|
@ -72,24 +71,25 @@ type TargetConfig struct {
|
|||
func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) {
|
||||
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
|
||||
content, err := fs.Read(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if !fs.Exists(configPath) {
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("build.LoadConfig: failed to read config file: %w", err)
|
||||
|
||||
// Use centralized config service
|
||||
c, err := config.New(config.WithMedium(fs), config.WithPath(configPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build.LoadConfig: %w", err)
|
||||
}
|
||||
|
||||
var cfg BuildConfig
|
||||
data := []byte(content)
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("build.LoadConfig: failed to parse config file: %w", err)
|
||||
cfg := DefaultConfig()
|
||||
if err := c.Get("", cfg); err != nil {
|
||||
return nil, fmt.Errorf("build.LoadConfig: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults for any missing fields
|
||||
applyDefaults(&cfg)
|
||||
// Apply defaults for any missing fields (centralized Get might not fill everything)
|
||||
applyDefaults(cfg)
|
||||
|
||||
return &cfg, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults for Go projects.
|
||||
|
|
@ -156,7 +156,7 @@ func ConfigPath(dir string) string {
|
|||
|
||||
// ConfigExists checks if a build config file exists in the given directory.
|
||||
func ConfigExists(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, ConfigPath(dir))
|
||||
return fs.IsFile(ConfigPath(dir))
|
||||
}
|
||||
|
||||
// ToTargets converts TargetConfig slice to Target slice for use with builders.
|
||||
|
|
|
|||
|
|
@ -13,27 +13,30 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
core "github.com/host-uk/core/pkg/framework/core"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
coreio "github.com/host-uk/core/pkg/io"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config implements the core.Config interface with layered resolution.
|
||||
// Values are resolved in order: defaults -> file -> env -> flags.
|
||||
// It uses viper as the underlying configuration engine.
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
medium io.Medium
|
||||
v *viper.Viper
|
||||
medium coreio.Medium
|
||||
path string
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
// Option is a functional option for configuring a Config instance.
|
||||
type Option func(*Config)
|
||||
|
||||
// WithMedium sets the storage medium for configuration file operations.
|
||||
func WithMedium(m io.Medium) Option {
|
||||
func WithMedium(m coreio.Medium) Option {
|
||||
return func(c *Config) {
|
||||
c.medium = m
|
||||
}
|
||||
|
|
@ -46,20 +49,31 @@ func WithPath(path string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEnvPrefix sets the prefix for environment variables.
|
||||
func WithEnvPrefix(prefix string) Option {
|
||||
return func(c *Config) {
|
||||
c.v.SetEnvPrefix(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Config instance with the given options.
|
||||
// If no medium is provided, it defaults to io.Local.
|
||||
// If no path is provided, it defaults to ~/.core/config.yaml.
|
||||
func New(opts ...Option) (*Config, error) {
|
||||
c := &Config{
|
||||
data: make(map[string]any),
|
||||
v: viper.New(),
|
||||
}
|
||||
|
||||
// Configure viper defaults
|
||||
c.v.SetEnvPrefix("CORE_CONFIG")
|
||||
c.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
if c.medium == nil {
|
||||
c.medium = io.Local
|
||||
c.medium = coreio.Local
|
||||
}
|
||||
|
||||
if c.path == "" {
|
||||
|
|
@ -67,40 +81,64 @@ func New(opts ...Option) (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, core.E("config.New", "failed to determine home directory", err)
|
||||
}
|
||||
c.path = home + "/.core/config.yaml"
|
||||
c.path = filepath.Join(home, ".core", "config.yaml")
|
||||
}
|
||||
|
||||
c.v.AutomaticEnv()
|
||||
|
||||
// Load existing config file if it exists
|
||||
if c.medium.IsFile(c.path) {
|
||||
loaded, err := Load(c.medium, c.path)
|
||||
if err != nil {
|
||||
if c.medium.Exists(c.path) {
|
||||
if err := c.LoadFile(c.medium, c.path); err != nil {
|
||||
return nil, core.E("config.New", "failed to load config file", err)
|
||||
}
|
||||
c.data = loaded
|
||||
}
|
||||
|
||||
// Overlay environment variables
|
||||
envData := LoadEnv("CORE_CONFIG_")
|
||||
for k, v := range envData {
|
||||
setNested(c.data, k, v)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// LoadFile reads a configuration file from the given medium and path and merges it into the current config.
|
||||
// It supports YAML and environment files (.env).
|
||||
func (c *Config) LoadFile(m coreio.Medium, path string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return core.E("config.LoadFile", "failed to read config file: "+path, err)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
if ext == "" && filepath.Base(path) == ".env" {
|
||||
c.v.SetConfigType("env")
|
||||
} else if ext != "" {
|
||||
c.v.SetConfigType(strings.TrimPrefix(ext, "."))
|
||||
} else {
|
||||
c.v.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
if err := c.v.MergeConfig(strings.NewReader(content)); err != nil {
|
||||
return core.E("config.LoadFile", "failed to parse config file: "+path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value by dot-notation key and stores it in out.
|
||||
// If key is empty, it unmarshals the entire configuration into out.
|
||||
// The out parameter must be a pointer to the target type.
|
||||
// Returns an error if the key is not found.
|
||||
func (c *Config) Get(key string, out any) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
val, ok := getNested(c.data, key)
|
||||
if !ok {
|
||||
if key == "" {
|
||||
return c.v.Unmarshal(out)
|
||||
}
|
||||
|
||||
if !c.v.IsSet(key) {
|
||||
return core.E("config.Get", fmt.Sprintf("key not found: %s", key), nil)
|
||||
}
|
||||
|
||||
return assign(val, out)
|
||||
return c.v.UnmarshalKey(key, out)
|
||||
}
|
||||
|
||||
// Set stores a configuration value by dot-notation key and persists to disk.
|
||||
|
|
@ -108,9 +146,10 @@ func (c *Config) Set(key string, v any) error {
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
setNested(c.data, key, v)
|
||||
c.v.Set(key, v)
|
||||
|
||||
if err := Save(c.medium, c.path, c.data); err != nil {
|
||||
// Persist to disk
|
||||
if err := Save(c.medium, c.path, c.v.AllSettings()); err != nil {
|
||||
return core.E("config.Set", "failed to save config", err)
|
||||
}
|
||||
|
||||
|
|
@ -122,25 +161,7 @@ func (c *Config) All() map[string]any {
|
|||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return deepCopyMap(c.data)
|
||||
}
|
||||
|
||||
// deepCopyMap recursively copies a map[string]any.
|
||||
func deepCopyMap(src map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(src))
|
||||
for k, v := range src {
|
||||
switch val := v.(type) {
|
||||
case map[string]any:
|
||||
result[k] = deepCopyMap(val)
|
||||
case []any:
|
||||
cp := make([]any, len(val))
|
||||
copy(cp, val)
|
||||
result[k] = cp
|
||||
default:
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
return c.v.AllSettings()
|
||||
}
|
||||
|
||||
// Path returns the path to the configuration file.
|
||||
|
|
@ -148,105 +169,41 @@ func (c *Config) Path() string {
|
|||
return c.path
|
||||
}
|
||||
|
||||
// getNested retrieves a value from a nested map using dot-notation keys.
|
||||
func getNested(data map[string]any, key string) (any, bool) {
|
||||
parts := strings.Split(key, ".")
|
||||
current := any(data)
|
||||
|
||||
for i, part := range parts {
|
||||
m, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
val, exists := m[part]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
if i == len(parts)-1 {
|
||||
return val, true
|
||||
}
|
||||
current = val
|
||||
// Load reads a YAML configuration file from the given medium and path.
|
||||
// Returns the parsed data as a map, or an error if the file cannot be read or parsed.
|
||||
// Deprecated: Use Config.LoadFile instead.
|
||||
func Load(m coreio.Medium, path string) (map[string]any, error) {
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, core.E("config.Load", "failed to read config file: "+path, err)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
if err := v.ReadConfig(strings.NewReader(content)); err != nil {
|
||||
return nil, core.E("config.Load", "failed to parse config file: "+path, err)
|
||||
}
|
||||
|
||||
return v.AllSettings(), nil
|
||||
}
|
||||
|
||||
// setNested sets a value in a nested map using dot-notation keys,
|
||||
// creating intermediate maps as needed.
|
||||
func setNested(data map[string]any, key string, value any) {
|
||||
parts := strings.Split(key, ".")
|
||||
current := data
|
||||
// Save writes configuration data to a YAML file at the given path.
|
||||
// It ensures the parent directory exists before writing.
|
||||
func Save(m coreio.Medium, path string, data map[string]any) error {
|
||||
out, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return core.E("config.Save", "failed to marshal config", err)
|
||||
}
|
||||
|
||||
for i, part := range parts {
|
||||
if i == len(parts)-1 {
|
||||
current[part] = value
|
||||
return
|
||||
dir := filepath.Dir(path)
|
||||
if err := m.EnsureDir(dir); err != nil {
|
||||
return core.E("config.Save", "failed to create config directory: "+dir, err)
|
||||
}
|
||||
next, ok := current[part]
|
||||
if !ok {
|
||||
next = make(map[string]any)
|
||||
current[part] = next
|
||||
}
|
||||
m, ok := next.(map[string]any)
|
||||
if !ok {
|
||||
m = make(map[string]any)
|
||||
current[part] = m
|
||||
}
|
||||
current = m
|
||||
}
|
||||
}
|
||||
|
||||
// assign sets the value of out to val, handling type conversions.
|
||||
func assign(val any, out any) error {
|
||||
switch ptr := out.(type) {
|
||||
case *string:
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
*ptr = v
|
||||
default:
|
||||
*ptr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
case *int:
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
*ptr = v
|
||||
case float64:
|
||||
*ptr = int(v)
|
||||
case int64:
|
||||
*ptr = int(v)
|
||||
default:
|
||||
return core.E("config.assign", fmt.Sprintf("cannot assign %T to *int", val), nil)
|
||||
}
|
||||
case *bool:
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
*ptr = v
|
||||
default:
|
||||
return core.E("config.assign", fmt.Sprintf("cannot assign %T to *bool", val), nil)
|
||||
}
|
||||
case *float64:
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
*ptr = v
|
||||
case int:
|
||||
*ptr = float64(v)
|
||||
case int64:
|
||||
*ptr = float64(v)
|
||||
default:
|
||||
return core.E("config.assign", fmt.Sprintf("cannot assign %T to *float64", val), nil)
|
||||
}
|
||||
case *any:
|
||||
*ptr = val
|
||||
case *map[string]any:
|
||||
switch v := val.(type) {
|
||||
case map[string]any:
|
||||
*ptr = v
|
||||
default:
|
||||
return core.E("config.assign", fmt.Sprintf("cannot assign %T to *map[string]any", val), nil)
|
||||
}
|
||||
default:
|
||||
return core.E("config.assign", fmt.Sprintf("unsupported target type: %T", out), nil)
|
||||
if err := m.Write(path, string(out)); err != nil {
|
||||
return core.E("config.Save", "failed to write config file: "+path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -225,3 +225,53 @@ func TestSave_Good(t *testing.T) {
|
|||
assert.NoError(t, readErr)
|
||||
assert.Contains(t, content, "key: value")
|
||||
}
|
||||
|
||||
func TestConfig_LoadFile_Env(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
m.Files["/.env"] = "FOO=bar\nBAZ=qux"
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = cfg.LoadFile(m, "/.env")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var foo string
|
||||
err = cfg.Get("foo", &foo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bar", foo)
|
||||
}
|
||||
|
||||
func TestConfig_WithEnvPrefix(t *testing.T) {
|
||||
t.Setenv("MYAPP_SETTING", "secret")
|
||||
|
||||
m := io.NewMockMedium()
|
||||
cfg, err := New(WithMedium(m), WithEnvPrefix("MYAPP"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var setting string
|
||||
err = cfg.Get("setting", &setting)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secret", setting)
|
||||
}
|
||||
|
||||
func TestConfig_Get_EmptyKey(t *testing.T) {
|
||||
m := io.NewMockMedium()
|
||||
m.Files["/config.yaml"] = "app:\n name: test\nversion: 1"
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
type AppConfig struct {
|
||||
App struct {
|
||||
Name string `mapstructure:"name"`
|
||||
} `mapstructure:"app"`
|
||||
Version int `mapstructure:"version"`
|
||||
}
|
||||
|
||||
var full AppConfig
|
||||
err = cfg.Get("", &full)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", full.App.Name)
|
||||
assert.Equal(t, 1, full.Version)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
core "github.com/host-uk/core/pkg/framework/core"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Load reads a YAML configuration file from the given medium and path.
|
||||
// Returns the parsed data as a map, or an error if the file cannot be read or parsed.
|
||||
func Load(m io.Medium, path string) (map[string]any, error) {
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
return nil, core.E("config.Load", "failed to read config file: "+path, err)
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
|
||||
return nil, core.E("config.Load", "failed to parse config file: "+path, err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Save writes configuration data to a YAML file at the given path.
|
||||
// It ensures the parent directory exists before writing.
|
||||
func Save(m io.Medium, path string, data map[string]any) error {
|
||||
out, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return core.E("config.Save", "failed to marshal config", err)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if err := m.EnsureDir(dir); err != nil {
|
||||
return core.E("config.Save", "failed to create config directory: "+dir, err)
|
||||
}
|
||||
|
||||
if err := m.Write(path, string(out)); err != nil {
|
||||
return core.E("config.Save", "failed to write config file: "+path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -67,6 +67,14 @@ func (s *Service) Set(key string, v any) error {
|
|||
return s.config.Set(key, v)
|
||||
}
|
||||
|
||||
// LoadFile merges a configuration file into the central configuration.
|
||||
func (s *Service) LoadFile(m io.Medium, path string) error {
|
||||
if s.config == nil {
|
||||
return core.E("config.Service.LoadFile", "config not loaded", nil)
|
||||
}
|
||||
return s.config.LoadFile(m, path)
|
||||
}
|
||||
|
||||
// Ensure Service implements core.Config and Startable at compile time.
|
||||
var (
|
||||
_ core.Config = (*Service)(nil)
|
||||
|
|
|
|||
|
|
@ -4,37 +4,37 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/config"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds global devops configuration from ~/.core/config.yaml.
|
||||
type Config struct {
|
||||
Version int `yaml:"version"`
|
||||
Images ImagesConfig `yaml:"images"`
|
||||
Version int `yaml:"version" mapstructure:"version"`
|
||||
Images ImagesConfig `yaml:"images" mapstructure:"images"`
|
||||
}
|
||||
|
||||
// ImagesConfig holds image source configuration.
|
||||
type ImagesConfig struct {
|
||||
Source string `yaml:"source"` // auto, github, registry, cdn
|
||||
GitHub GitHubConfig `yaml:"github,omitempty"`
|
||||
Registry RegistryConfig `yaml:"registry,omitempty"`
|
||||
CDN CDNConfig `yaml:"cdn,omitempty"`
|
||||
Source string `yaml:"source" mapstructure:"source"` // auto, github, registry, cdn
|
||||
GitHub GitHubConfig `yaml:"github,omitempty" mapstructure:"github,omitempty"`
|
||||
Registry RegistryConfig `yaml:"registry,omitempty" mapstructure:"registry,omitempty"`
|
||||
CDN CDNConfig `yaml:"cdn,omitempty" mapstructure:"cdn,omitempty"`
|
||||
}
|
||||
|
||||
// GitHubConfig holds GitHub Releases configuration.
|
||||
type GitHubConfig struct {
|
||||
Repo string `yaml:"repo"` // owner/repo format
|
||||
Repo string `yaml:"repo" mapstructure:"repo"` // owner/repo format
|
||||
}
|
||||
|
||||
// RegistryConfig holds container registry configuration.
|
||||
type RegistryConfig struct {
|
||||
Image string `yaml:"image"` // e.g., ghcr.io/host-uk/core-devops
|
||||
Image string `yaml:"image" mapstructure:"image"` // e.g., ghcr.io/host-uk/core-devops
|
||||
}
|
||||
|
||||
// CDNConfig holds CDN/S3 configuration.
|
||||
type CDNConfig struct {
|
||||
URL string `yaml:"url"` // base URL for downloads
|
||||
URL string `yaml:"url" mapstructure:"url"` // base URL for downloads
|
||||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults.
|
||||
|
|
@ -70,16 +70,19 @@ func LoadConfig(m io.Medium) (*Config, error) {
|
|||
return DefaultConfig(), nil
|
||||
}
|
||||
|
||||
content, err := m.Read(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return DefaultConfig(), nil
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if !m.IsFile(configPath) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Use centralized config service
|
||||
c, err := config.New(config.WithMedium(m), config.WithPath(configPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := yaml.Unmarshal([]byte(content), cfg); err != nil {
|
||||
if err := c.Get("", cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package release
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/config"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -19,17 +19,17 @@ const ConfigDir = ".core"
|
|||
// Config holds the complete release configuration loaded from .core/release.yaml.
|
||||
type Config struct {
|
||||
// Version is the config file format version.
|
||||
Version int `yaml:"version"`
|
||||
Version int `yaml:"version" mapstructure:"version"`
|
||||
// Project contains project metadata.
|
||||
Project ProjectConfig `yaml:"project"`
|
||||
Project ProjectConfig `yaml:"project" mapstructure:"project"`
|
||||
// Build contains build settings for the release.
|
||||
Build BuildConfig `yaml:"build"`
|
||||
Build BuildConfig `yaml:"build" mapstructure:"build"`
|
||||
// Publishers defines where to publish the release.
|
||||
Publishers []PublisherConfig `yaml:"publishers"`
|
||||
Publishers []PublisherConfig `yaml:"publishers" mapstructure:"publishers"`
|
||||
// Changelog configures changelog generation.
|
||||
Changelog ChangelogConfig `yaml:"changelog"`
|
||||
Changelog ChangelogConfig `yaml:"changelog" mapstructure:"changelog"`
|
||||
// SDK configures SDK generation.
|
||||
SDK *SDKConfig `yaml:"sdk,omitempty"`
|
||||
SDK *SDKConfig `yaml:"sdk,omitempty" mapstructure:"sdk,omitempty"`
|
||||
|
||||
// Internal fields (not serialized)
|
||||
projectDir string // Set by LoadConfig
|
||||
|
|
@ -39,131 +39,131 @@ type Config struct {
|
|||
// ProjectConfig holds project metadata for releases.
|
||||
type ProjectConfig struct {
|
||||
// Name is the project name.
|
||||
Name string `yaml:"name"`
|
||||
Name string `yaml:"name" mapstructure:"name"`
|
||||
// Repository is the GitHub repository in owner/repo format.
|
||||
Repository string `yaml:"repository"`
|
||||
Repository string `yaml:"repository" mapstructure:"repository"`
|
||||
}
|
||||
|
||||
// BuildConfig holds build settings for releases.
|
||||
type BuildConfig struct {
|
||||
// Targets defines the build targets.
|
||||
Targets []TargetConfig `yaml:"targets"`
|
||||
Targets []TargetConfig `yaml:"targets" mapstructure:"targets"`
|
||||
}
|
||||
|
||||
// TargetConfig defines a build target.
|
||||
type TargetConfig struct {
|
||||
// OS is the target operating system (e.g., "linux", "darwin", "windows").
|
||||
OS string `yaml:"os"`
|
||||
OS string `yaml:"os" mapstructure:"os"`
|
||||
// Arch is the target architecture (e.g., "amd64", "arm64").
|
||||
Arch string `yaml:"arch"`
|
||||
Arch string `yaml:"arch" mapstructure:"arch"`
|
||||
}
|
||||
|
||||
// PublisherConfig holds configuration for a publisher.
|
||||
type PublisherConfig struct {
|
||||
// Type is the publisher type (e.g., "github", "linuxkit", "docker").
|
||||
Type string `yaml:"type"`
|
||||
Type string `yaml:"type" mapstructure:"type"`
|
||||
// Prerelease marks the release as a prerelease.
|
||||
Prerelease bool `yaml:"prerelease"`
|
||||
Prerelease bool `yaml:"prerelease" mapstructure:"prerelease"`
|
||||
// Draft creates the release as a draft.
|
||||
Draft bool `yaml:"draft"`
|
||||
Draft bool `yaml:"draft" mapstructure:"draft"`
|
||||
|
||||
// LinuxKit-specific configuration
|
||||
// Config is the path to the LinuxKit YAML configuration file.
|
||||
Config string `yaml:"config,omitempty"`
|
||||
Config string `yaml:"config,omitempty" mapstructure:"config,omitempty"`
|
||||
// Formats are the output formats to build (iso, raw, qcow2, vmdk).
|
||||
Formats []string `yaml:"formats,omitempty"`
|
||||
Formats []string `yaml:"formats,omitempty" mapstructure:"formats,omitempty"`
|
||||
// Platforms are the target platforms (linux/amd64, linux/arm64).
|
||||
Platforms []string `yaml:"platforms,omitempty"`
|
||||
Platforms []string `yaml:"platforms,omitempty" mapstructure:"platforms,omitempty"`
|
||||
|
||||
// Docker-specific configuration
|
||||
// Registry is the container registry (default: ghcr.io).
|
||||
Registry string `yaml:"registry,omitempty"`
|
||||
Registry string `yaml:"registry,omitempty" mapstructure:"registry,omitempty"`
|
||||
// Image is the image name in owner/repo format.
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Image string `yaml:"image,omitempty" mapstructure:"image,omitempty"`
|
||||
// Dockerfile is the path to the Dockerfile (default: Dockerfile).
|
||||
Dockerfile string `yaml:"dockerfile,omitempty"`
|
||||
Dockerfile string `yaml:"dockerfile,omitempty" mapstructure:"dockerfile,omitempty"`
|
||||
// Tags are the image tags to apply.
|
||||
Tags []string `yaml:"tags,omitempty"`
|
||||
Tags []string `yaml:"tags,omitempty" mapstructure:"tags,omitempty"`
|
||||
// BuildArgs are additional Docker build arguments.
|
||||
BuildArgs map[string]string `yaml:"build_args,omitempty"`
|
||||
BuildArgs map[string]string `yaml:"build_args,omitempty" mapstructure:"build_args,omitempty"`
|
||||
|
||||
// npm-specific configuration
|
||||
// Package is the npm package name (e.g., "@host-uk/core").
|
||||
Package string `yaml:"package,omitempty"`
|
||||
Package string `yaml:"package,omitempty" mapstructure:"package,omitempty"`
|
||||
// Access is the npm access level: "public" or "restricted".
|
||||
Access string `yaml:"access,omitempty"`
|
||||
Access string `yaml:"access,omitempty" mapstructure:"access,omitempty"`
|
||||
|
||||
// Homebrew-specific configuration
|
||||
// Tap is the Homebrew tap repository (e.g., "host-uk/homebrew-tap").
|
||||
Tap string `yaml:"tap,omitempty"`
|
||||
Tap string `yaml:"tap,omitempty" mapstructure:"tap,omitempty"`
|
||||
// Formula is the formula name (defaults to project name).
|
||||
Formula string `yaml:"formula,omitempty"`
|
||||
Formula string `yaml:"formula,omitempty" mapstructure:"formula,omitempty"`
|
||||
|
||||
// Scoop-specific configuration
|
||||
// Bucket is the Scoop bucket repository (e.g., "host-uk/scoop-bucket").
|
||||
Bucket string `yaml:"bucket,omitempty"`
|
||||
Bucket string `yaml:"bucket,omitempty" mapstructure:"bucket,omitempty"`
|
||||
|
||||
// AUR-specific configuration
|
||||
// Maintainer is the AUR package maintainer (e.g., "Name <email>").
|
||||
Maintainer string `yaml:"maintainer,omitempty"`
|
||||
Maintainer string `yaml:"maintainer,omitempty" mapstructure:"maintainer,omitempty"`
|
||||
|
||||
// Chocolatey-specific configuration
|
||||
// Push determines whether to push to Chocolatey (false = generate only).
|
||||
Push bool `yaml:"push,omitempty"`
|
||||
Push bool `yaml:"push,omitempty" mapstructure:"push,omitempty"`
|
||||
|
||||
// Official repo configuration (for Homebrew, Scoop)
|
||||
// When enabled, generates files for PR to official repos.
|
||||
Official *OfficialConfig `yaml:"official,omitempty"`
|
||||
Official *OfficialConfig `yaml:"official,omitempty" mapstructure:"official,omitempty"`
|
||||
}
|
||||
|
||||
// OfficialConfig holds configuration for generating files for official repo PRs.
|
||||
type OfficialConfig struct {
|
||||
// Enabled determines whether to generate files for official repos.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
|
||||
// Output is the directory to write generated files.
|
||||
Output string `yaml:"output,omitempty"`
|
||||
Output string `yaml:"output,omitempty" mapstructure:"output,omitempty"`
|
||||
}
|
||||
|
||||
// SDKConfig holds SDK generation configuration.
|
||||
type SDKConfig struct {
|
||||
// Spec is the path to the OpenAPI spec file.
|
||||
Spec string `yaml:"spec,omitempty"`
|
||||
Spec string `yaml:"spec,omitempty" mapstructure:"spec,omitempty"`
|
||||
// Languages to generate.
|
||||
Languages []string `yaml:"languages,omitempty"`
|
||||
Languages []string `yaml:"languages,omitempty" mapstructure:"languages,omitempty"`
|
||||
// Output directory (default: sdk/).
|
||||
Output string `yaml:"output,omitempty"`
|
||||
Output string `yaml:"output,omitempty" mapstructure:"output,omitempty"`
|
||||
// Package naming.
|
||||
Package SDKPackageConfig `yaml:"package,omitempty"`
|
||||
Package SDKPackageConfig `yaml:"package,omitempty" mapstructure:"package,omitempty"`
|
||||
// Diff configuration.
|
||||
Diff SDKDiffConfig `yaml:"diff,omitempty"`
|
||||
Diff SDKDiffConfig `yaml:"diff,omitempty" mapstructure:"diff,omitempty"`
|
||||
// Publish configuration.
|
||||
Publish SDKPublishConfig `yaml:"publish,omitempty"`
|
||||
Publish SDKPublishConfig `yaml:"publish,omitempty" mapstructure:"publish,omitempty"`
|
||||
}
|
||||
|
||||
// SDKPackageConfig holds package naming configuration.
|
||||
type SDKPackageConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Version string `yaml:"version,omitempty"`
|
||||
Name string `yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
Version string `yaml:"version,omitempty" mapstructure:"version,omitempty"`
|
||||
}
|
||||
|
||||
// SDKDiffConfig holds diff configuration.
|
||||
type SDKDiffConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
FailOnBreaking bool `yaml:"fail_on_breaking,omitempty"`
|
||||
Enabled bool `yaml:"enabled,omitempty" mapstructure:"enabled,omitempty"`
|
||||
FailOnBreaking bool `yaml:"fail_on_breaking,omitempty" mapstructure:"fail_on_breaking,omitempty"`
|
||||
}
|
||||
|
||||
// SDKPublishConfig holds monorepo publish configuration.
|
||||
type SDKPublishConfig struct {
|
||||
Repo string `yaml:"repo,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Repo string `yaml:"repo,omitempty" mapstructure:"repo,omitempty"`
|
||||
Path string `yaml:"path,omitempty" mapstructure:"path,omitempty"`
|
||||
}
|
||||
|
||||
// ChangelogConfig holds changelog generation settings.
|
||||
type ChangelogConfig struct {
|
||||
// Include specifies commit types to include in the changelog.
|
||||
Include []string `yaml:"include"`
|
||||
Include []string `yaml:"include" mapstructure:"include"`
|
||||
// Exclude specifies commit types to exclude from the changelog.
|
||||
Exclude []string `yaml:"exclude"`
|
||||
Exclude []string `yaml:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
// LoadConfig loads release configuration from the .core/release.yaml file in the given directory.
|
||||
|
|
@ -176,26 +176,28 @@ func LoadConfig(m io.Medium, dir string) (*Config, error) {
|
|||
return nil, fmt.Errorf("release.LoadConfig: failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
content, err := m.Read(absPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if !m.Exists(absPath) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.projectDir = dir
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("release.LoadConfig: failed to read config file: %w", err)
|
||||
|
||||
// Use centralized config service
|
||||
c, err := config.New(config.WithMedium(m), config.WithPath(absPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release.LoadConfig: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
|
||||
return nil, fmt.Errorf("release.LoadConfig: failed to parse config file: %w", err)
|
||||
cfg := DefaultConfig()
|
||||
if err := c.Get("", cfg); err != nil {
|
||||
return nil, fmt.Errorf("release.LoadConfig: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults for any missing fields
|
||||
applyDefaults(&cfg)
|
||||
applyDefaults(cfg)
|
||||
cfg.projectDir = dir
|
||||
|
||||
return &cfg, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults for release configuration.
|
||||
|
|
@ -272,7 +274,7 @@ func ConfigExists(m io.Medium, dir string) bool {
|
|||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return m.IsFile(absPath)
|
||||
return m.Exists(absPath)
|
||||
}
|
||||
|
||||
// GetRepository returns the repository from the config.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue