Add campaign configuration support
This commit is contained in:
138
internal/config/campaign_config_test.go
Normal file
138
internal/config/campaign_config_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCampaignConfigDefaultSearchOrder(t *testing.T) {
|
||||
want := []string{
|
||||
"./campaign.yml",
|
||||
"/usr/local/etc/narratio/campaign.yml",
|
||||
"/etc/narratio/campaign.yml",
|
||||
}
|
||||
if len(DefaultCampaignConfigSearchPaths) != len(want) {
|
||||
t.Fatalf("DefaultCampaignConfigSearchPaths = %#v, want %#v", DefaultCampaignConfigSearchPaths, want)
|
||||
}
|
||||
for i := range want {
|
||||
if DefaultCampaignConfigSearchPaths[i] != want[i] {
|
||||
t.Fatalf("DefaultCampaignConfigSearchPaths[%d] = %q, want %q", i, DefaultCampaignConfigSearchPaths[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected load error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "campaign file") || !strings.Contains(err.Error(), "strict decode failed") {
|
||||
t.Fatalf("error = %q, want campaign strict decode context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
if err := Validate(cfg); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if cfg.Session.Campaign != "sample-campaign" {
|
||||
t.Fatalf("session campaign = %q, want campaign config value", cfg.Session.Campaign)
|
||||
}
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
}
|
||||
|
||||
func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n",
|
||||
)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
if err := Validate(cfg); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
}
|
||||
|
||||
func TestCampaignSessionMismatchFails(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: other-campaign\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected load error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "does not match campaign config") {
|
||||
t.Fatalf("error = %q, want campaign mismatch context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMissingCampaignFileFails(t *testing.T) {
|
||||
pipelinePath, _, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
missingCampaignPath := filepath.Join(filepath.Dir(sessionPath), "missing-campaign.yml")
|
||||
|
||||
_, err := LoadWithSessionOptions(pipelinePath, missingCampaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected load error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "load campaign config") {
|
||||
t.Fatalf("error = %q, want campaign load context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func writeCampaignConfigTestFiles(t *testing.T, campaignYAML, sessionYAML string) (string, string, string) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||
sessionPath := filepath.Join(dir, "session.yml")
|
||||
|
||||
pipelineYAML := "workspace:\n root: " + filepath.ToSlash(filepath.Join(dir, "work")) + "\nwhisperx:\n transcribe_url: https://example.com/transcribe\nanalyzer:\n timeout: 20m\nnotification:\n timeout: 10s\n"
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign.yml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
}
|
||||
|
||||
return pipelinePath, campaignPath, sessionPath
|
||||
}
|
||||
|
||||
func assertResolvedStableInput(t *testing.T, got ResolvedInputFile, wantPath, wantConfigPath, wantSource string) {
|
||||
t.Helper()
|
||||
if got.Path != wantPath || got.ConfigPath != wantConfigPath || got.Source != wantSource {
|
||||
t.Fatalf("resolved input = %#v, want path=%q config_path=%q source=%q", got, wantPath, wantConfigPath, wantSource)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
package config
|
||||
|
||||
// Config is the resolved combined configuration from pipeline.yml and session.yml.
|
||||
// Config is the resolved combined configuration from pipeline.yml,
|
||||
// campaign.yml, and session.yml.
|
||||
type Config struct {
|
||||
Pipeline *PipelineConfig
|
||||
Campaign *CampaignConfig
|
||||
Session *SessionConfig
|
||||
PipelinePath string
|
||||
CampaignPath string
|
||||
SessionPath string
|
||||
|
||||
StableInputs ResolvedStableInputs
|
||||
}
|
||||
|
||||
// PipelineConfig contains durable pipeline-level settings.
|
||||
@@ -25,6 +30,19 @@ type PipelineConfig struct {
|
||||
Notification NotificationConfig `yaml:"notification"`
|
||||
}
|
||||
|
||||
// CampaignConfig contains stable campaign-level identity and input defaults.
|
||||
type CampaignConfig struct {
|
||||
Campaign string `yaml:"campaign"`
|
||||
Inputs CampaignInputsConfig `yaml:"inputs"`
|
||||
}
|
||||
|
||||
// CampaignInputsConfig contains stable campaign-level input file references.
|
||||
type CampaignInputsConfig struct {
|
||||
SpeakersFile string `yaml:"speakers_file"`
|
||||
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||
GlossaryFile string `yaml:"glossary_file"`
|
||||
}
|
||||
|
||||
// SessionConfig contains per-session inputs and metadata.
|
||||
type SessionConfig struct {
|
||||
SessionID string `yaml:"session_id"`
|
||||
@@ -229,3 +247,18 @@ type SessionInputsConfig struct {
|
||||
type SessionAudioS3Input struct {
|
||||
Prefix string `yaml:"prefix"`
|
||||
}
|
||||
|
||||
// ResolvedStableInputs records where stable input file paths came from after
|
||||
// campaign/session merge.
|
||||
type ResolvedStableInputs struct {
|
||||
SpeakersFile ResolvedInputFile
|
||||
AutocorrectFile ResolvedInputFile
|
||||
GlossaryFile ResolvedInputFile
|
||||
}
|
||||
|
||||
// ResolvedInputFile records one merged config path and its source config file.
|
||||
type ResolvedInputFile struct {
|
||||
Path string
|
||||
ConfigPath string
|
||||
Source string
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ package config
|
||||
const (
|
||||
DefaultPipelineConfigPathUsrLocal = "/usr/local/etc/narratio/pipeline.yml"
|
||||
DefaultPipelineConfigPathEtc = "/etc/narratio/pipeline.yml"
|
||||
DefaultCampaignConfigPathLocal = "./campaign.yml"
|
||||
DefaultCampaignConfigPathUsrLocal = "/usr/local/etc/narratio/campaign.yml"
|
||||
DefaultCampaignConfigPathEtc = "/etc/narratio/campaign.yml"
|
||||
DefaultSessionConfigPathLocal = "./session.yml"
|
||||
DefaultSessionConfigPathUsrLocal = "/usr/local/etc/narratio/session.yml"
|
||||
DefaultSessionConfigPathEtc = "/etc/narratio/session.yml"
|
||||
@@ -88,6 +91,17 @@ var DefaultPipelineConfigSearchPaths = []string{
|
||||
DefaultPipelineConfigPathEtc,
|
||||
}
|
||||
|
||||
// DefaultCampaignConfigSearchPaths defines the default search order for
|
||||
// campaign.yml when callers do not provide an explicit path.
|
||||
//
|
||||
// Keep this in a variable so future defaults can be extended without changing
|
||||
// call sites.
|
||||
var DefaultCampaignConfigSearchPaths = []string{
|
||||
DefaultCampaignConfigPathLocal,
|
||||
DefaultCampaignConfigPathUsrLocal,
|
||||
DefaultCampaignConfigPathEtc,
|
||||
}
|
||||
|
||||
// DefaultSessionConfigSearchPaths defines the default search order for
|
||||
// session.yml when callers do not provide an explicit path.
|
||||
//
|
||||
|
||||
@@ -22,6 +22,15 @@ func LoadPipeline(path string) (*PipelineConfig, error) {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// LoadCampaign loads campaign configuration from a YAML file with strict field checking.
|
||||
func LoadCampaign(path string) (*CampaignConfig, error) {
|
||||
var cfg CampaignConfig
|
||||
if err := decodeStrictYAML("campaign", path, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("load campaign config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// LoadSession loads session configuration from a YAML file with strict field checking.
|
||||
func LoadSession(path string) (*SessionConfig, error) {
|
||||
return LoadSessionWithOptions(path, SessionLoadOptions{})
|
||||
@@ -71,32 +80,128 @@ func LoadSessionWithOptions(path string, opts SessionLoadOptions) (*SessionConfi
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Load loads and resolves combined pipeline and session configuration.
|
||||
func Load(pipelinePath, sessionPath string) (*Config, error) {
|
||||
return LoadWithSessionOptions(pipelinePath, sessionPath, SessionLoadOptions{})
|
||||
// Load loads and resolves combined pipeline, campaign, and session configuration.
|
||||
// Passing only a session path is supported for package-internal compatibility;
|
||||
// in that form campaign.yml is expected next to the session file.
|
||||
func Load(pipelinePath string, paths ...string) (*Config, error) {
|
||||
campaignPath, sessionPath, err := campaignSessionPaths(paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
}
|
||||
|
||||
// LoadWithSessionOptions loads and resolves combined pipeline and session
|
||||
// configuration with session template options.
|
||||
func LoadWithSessionOptions(pipelinePath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
||||
// LoadWithSessionOptions loads and resolves combined pipeline, campaign, and
|
||||
// session configuration with session template options.
|
||||
func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
||||
pipelineCfg, err := LoadPipeline(pipelinePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
campaignCfg, err := LoadCampaign(campaignPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionCfg, err := LoadSessionWithOptions(sessionPath, sessionOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stableInputs, err := mergeCampaignSession(campaignCfg, sessionCfg, campaignPath, sessionPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Pipeline: pipelineCfg,
|
||||
Campaign: campaignCfg,
|
||||
Session: sessionCfg,
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
SessionPath: sessionPath,
|
||||
StableInputs: stableInputs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func campaignSessionPaths(paths ...string) (campaignPath, sessionPath string, err error) {
|
||||
switch len(paths) {
|
||||
case 1:
|
||||
sessionPath = paths[0]
|
||||
campaignPath = filepath.Join(filepath.Dir(sessionPath), "campaign.yml")
|
||||
case 2:
|
||||
campaignPath = paths[0]
|
||||
sessionPath = paths[1]
|
||||
default:
|
||||
return "", "", fmt.Errorf("load config: expected session path or campaign and session paths")
|
||||
}
|
||||
return campaignPath, sessionPath, nil
|
||||
}
|
||||
|
||||
func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig, campaignPath, sessionPath string) (ResolvedStableInputs, error) {
|
||||
if campaignCfg == nil {
|
||||
return ResolvedStableInputs{}, fmt.Errorf("campaign config is required")
|
||||
}
|
||||
if sessionCfg == nil {
|
||||
return ResolvedStableInputs{}, fmt.Errorf("session config is required")
|
||||
}
|
||||
|
||||
campaignName := strings.TrimSpace(campaignCfg.Campaign)
|
||||
sessionCampaign := strings.TrimSpace(sessionCfg.Campaign)
|
||||
if sessionCampaign != "" && campaignName != "" && sessionCampaign != campaignName {
|
||||
return ResolvedStableInputs{}, fmt.Errorf(
|
||||
"campaign/session config invalid: session campaign %q does not match campaign config %q",
|
||||
sessionCampaign,
|
||||
campaignName,
|
||||
)
|
||||
}
|
||||
if sessionCampaign == "" {
|
||||
sessionCfg.Campaign = campaignName
|
||||
}
|
||||
|
||||
stable := ResolvedStableInputs{
|
||||
SpeakersFile: selectStableInput(
|
||||
campaignCfg.Inputs.SpeakersFile,
|
||||
sessionCfg.Inputs.SpeakersFile,
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
AutocorrectFile: selectStableInput(
|
||||
campaignCfg.Inputs.AutocorrectFile,
|
||||
sessionCfg.Inputs.AutocorrectFile,
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
GlossaryFile: selectStableInput(
|
||||
campaignCfg.Inputs.GlossaryFile,
|
||||
sessionCfg.Inputs.GlossaryFile,
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
}
|
||||
|
||||
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
||||
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
||||
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
||||
return stable, nil
|
||||
}
|
||||
|
||||
func selectStableInput(campaignValue, sessionValue, campaignPath, sessionPath string) ResolvedInputFile {
|
||||
if strings.TrimSpace(sessionValue) != "" {
|
||||
return ResolvedInputFile{
|
||||
Path: sessionValue,
|
||||
ConfigPath: sessionPath,
|
||||
Source: "session_config",
|
||||
}
|
||||
}
|
||||
return ResolvedInputFile{
|
||||
Path: campaignValue,
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
}
|
||||
}
|
||||
|
||||
func decodeStrictYAML(kind, path string, out any) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -904,6 +904,7 @@ func TestValidateMissingAudioSource(t *testing.T) {
|
||||
Report: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Campaign: &CampaignConfig{Campaign: "sample-campaign"},
|
||||
Session: &SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
Campaign: "sample-campaign",
|
||||
@@ -963,6 +964,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pipelinePath := filepath.Join(examplesDir, tt.pipelineFile)
|
||||
campaignPath := filepath.Join(examplesDir, "campaign.yml")
|
||||
sessionPath := filepath.Join(examplesDir, tt.sessionFile)
|
||||
|
||||
var (
|
||||
@@ -972,7 +974,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
||||
if strings.TrimSpace(tt.sessionOpts.SessionID) == "" {
|
||||
cfg, err = Load(pipelinePath, sessionPath)
|
||||
} else {
|
||||
cfg, err = LoadWithSessionOptions(pipelinePath, sessionPath, tt.sessionOpts)
|
||||
cfg, err = LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, tt.sessionOpts)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("load example config error = %v", err)
|
||||
@@ -1001,14 +1003,34 @@ func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, s
|
||||
|
||||
dir := t.TempDir()
|
||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||
sessionPath := filepath.Join(dir, "session.yml")
|
||||
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
}
|
||||
campaignYAML := `campaign: ` + campaignNameFromSessionYAML(sessionYAML) + `
|
||||
inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign.yml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
}
|
||||
|
||||
return pipelinePath, sessionPath
|
||||
}
|
||||
|
||||
func campaignNameFromSessionYAML(sessionYAML string) string {
|
||||
for _, line := range strings.Split(sessionYAML, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "campaign:") {
|
||||
return strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "campaign:")), `"'`)
|
||||
}
|
||||
}
|
||||
return "sample-campaign"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ func Validate(cfg *Config) error {
|
||||
if cfg.Pipeline == nil {
|
||||
return fmt.Errorf("pipeline config is required")
|
||||
}
|
||||
if cfg.Campaign == nil {
|
||||
return fmt.Errorf("campaign config is required")
|
||||
}
|
||||
if cfg.Session == nil {
|
||||
return fmt.Errorf("session config is required")
|
||||
}
|
||||
@@ -24,6 +27,9 @@ func Validate(cfg *Config) error {
|
||||
if err := validatePipeline(cfg.Pipeline); err != nil {
|
||||
return fmt.Errorf("pipeline config %q invalid: %w", shortName(cfg.PipelinePath, "pipeline.yml"), err)
|
||||
}
|
||||
if err := validateCampaign(cfg.Campaign); err != nil {
|
||||
return fmt.Errorf("campaign config %q invalid: %w", shortName(cfg.CampaignPath, "campaign.yml"), err)
|
||||
}
|
||||
if err := validateSession(cfg.Session); err != nil {
|
||||
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
|
||||
}
|
||||
@@ -34,6 +40,16 @@ func Validate(cfg *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCampaign(cfg *CampaignConfig) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("campaign config is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Campaign) == "" {
|
||||
return fmt.Errorf("campaign.campaign is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePipeline(cfg *PipelineConfig) error {
|
||||
if strings.TrimSpace(cfg.Workspace.Root) == "" {
|
||||
return fmt.Errorf("pipeline.workspace.root is required")
|
||||
|
||||
Reference in New Issue
Block a user