Rationalized the default configuration file path and updated documentation
All checks were successful
ci/woodpecker/tag/release Pipeline was successful

This commit is contained in:
2026-05-14 20:48:12 -05:00
parent b09966f076
commit 77134c3c78
4 changed files with 85 additions and 4 deletions

View File

@@ -2,7 +2,7 @@
`config.yml` defines application-level defaults used by CLI commands.
By default, Scriptorium looks for `/etc/scriptorium/config.yml`. You can also pass `--config PATH`.
By default, Scriptorium looks for `/usr/local/etc/scriptorium/config.yml` and, if not present, then for `/etc/scriptorium/config.yml`. You can also pass `--config PATH`.
## Complete Example

View File

@@ -450,7 +450,16 @@ func (c *serveConfig) addrIfSet(fs *flag.FlagSet) string {
}
func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
fs.StringVar(target, "config", "", fmt.Sprintf("application config path (default %s)", appconfig.DefaultConfigPath))
fs.StringVar(
target,
"config",
"",
fmt.Sprintf(
"application config path (default search: %s, then %s)",
appconfig.DefaultConfigPathLocal,
appconfig.DefaultConfigPath,
),
)
}
func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) {

View File

@@ -14,7 +14,15 @@ import (
"gopkg.in/yaml.v3"
)
const DefaultConfigPath = "/etc/scriptorium/config.yml"
const (
DefaultConfigPath = "/etc/scriptorium/config.yml"
DefaultConfigPathLocal = "/usr/local/etc/scriptorium/config.yml"
)
var defaultConfigSearchPaths = []string{
DefaultConfigPathLocal,
DefaultConfigPath,
}
var (
ErrConfigNotFound = errors.New("config file not found")
@@ -74,7 +82,24 @@ func LoadConfig(path string, explicit bool) (AppSettings, error) {
resolved := BuiltInDefaults()
configPath := strings.TrimSpace(path)
if configPath == "" {
configPath = DefaultConfigPath
if explicit {
configPath = DefaultConfigPath
} else {
for _, candidate := range defaultConfigSearchPaths {
if _, err := os.Stat(candidate); err == nil {
configPath = candidate
break
} else if !errors.Is(err, os.ErrNotExist) {
return AppSettings{}, fmt.Errorf("failed to stat config file %q: %w", candidate, err)
}
}
}
}
if configPath == "" {
if explicit {
return AppSettings{}, fmt.Errorf("%w: %s", ErrConfigNotFound, DefaultConfigPath)
}
return resolved, nil
}
configPath = filepath.Clean(configPath)

View File

@@ -132,6 +132,53 @@ func TestLoadConfigEmptyFileResolvesToBuiltInDefaults(t *testing.T) {
}
}
func TestLoadConfigImplicitSearchPrefersUsrLocalEtcOverEtc(t *testing.T) {
tmp := t.TempDir()
localPath := filepath.Join(tmp, "usr-local.yml")
etcPath := filepath.Join(tmp, "etc.yml")
if err := os.WriteFile(localPath, []byte("prompt_dir: ./from-usr-local\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil {
t.Fatal(err)
}
orig := defaultConfigSearchPaths
defaultConfigSearchPaths = []string{localPath, etcPath}
t.Cleanup(func() { defaultConfigSearchPaths = orig })
got, err := LoadConfig("", false)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got.PromptDir != filepath.Clean("./from-usr-local") {
t.Fatalf("expected usr-local config to win, got prompt_dir=%q", got.PromptDir)
}
}
func TestLoadConfigImplicitSearchFallsBackToEtcWhenUsrLocalMissing(t *testing.T) {
tmp := t.TempDir()
missingLocal := filepath.Join(tmp, "missing-local.yml")
etcPath := filepath.Join(tmp, "etc.yml")
if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil {
t.Fatal(err)
}
orig := defaultConfigSearchPaths
defaultConfigSearchPaths = []string{missingLocal, etcPath}
t.Cleanup(func() { defaultConfigSearchPaths = orig })
got, err := LoadConfig("", false)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got.PromptDir != filepath.Clean("./from-etc") {
t.Fatalf("expected etc fallback config, got prompt_dir=%q", got.PromptDir)
}
}
func TestApplyCLIOverridesAppliesPrecedence(t *testing.T) {
base := AppSettings{
PromptDir: "/from/config/prompts",