Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
"github.com/spf13/cobra"
)

const configFlagUsage = "config file (default is .tusk/config.yaml)"
const (
configFlagUsage = "config file (default is .tusk/config.yaml)"
configOverrideFlagUsage = "config override file (merges on top of the base config)"
)

//go:embed short_docs/drift/drift_overview.md
var driftOverviewContent string
Expand All @@ -21,6 +24,7 @@ var driftCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(driftCmd)
driftCmd.PersistentFlags().StringVar(&cfgFile, "config", "", configFlagUsage)
driftCmd.PersistentFlags().StringVar(&cfgOverrideFile, "config-override", "", configOverrideFlagUsage)
Comment thread
cursor[bot] marked this conversation as resolved.
}

func bindLegacyDriftAliasConfigFlag(cmd *cobra.Command) {
Expand Down
7 changes: 7 additions & 0 deletions cmd/drift_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ func init() {

// setupDriftQueryCloud sets up the API client and resolves the service ID.
func setupDriftQueryCloud(serviceIDFlag string) (*api.TuskClient, api.AuthOptions, string, error) {
// Ensure config is loaded with override support before Get() is called by SetupCloud
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
return nil, api.AuthOptions{}, "", fmt.Errorf("failed to load config: %w", err)
}
}

client, authOptions, cfg, err := api.SetupCloud(context.Background(), false)
if err != nil {
return nil, api.AuthOptions{}, "", err
Expand Down
10 changes: 10 additions & 0 deletions cmd/drift_query_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package cmd

import (
"context"
"fmt"
"os"

"github.com/Use-Tusk/tusk-cli/internal/api"
"github.com/Use-Tusk/tusk-cli/internal/config"
"github.com/spf13/cobra"
)

Expand All @@ -12,6 +15,13 @@ var driftQueryServicesCmd = &cobra.Command{
Short: "List available Tusk Drift Cloud services",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Ensure config is loaded with override support before Get() is called by SetupCloud
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
return fmt.Errorf("failed to load config: %w", err)
}
}

client, authOptions, _, err := api.SetupCloud(context.Background(), false)
if err != nil {
return formatApiError(err)
Expand Down
12 changes: 10 additions & 2 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ func bindListFlags(cmd *cobra.Command) {
func listTests(cmd *cobra.Command, args []string) error {
setupSignalHandling()

_ = config.Load(cfgFile)
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
return fmt.Errorf("failed to load config: %w", err)
}
}
cfg, getConfigErr := config.Get()

executor := runner.NewExecutor()
Expand Down Expand Up @@ -124,7 +128,11 @@ func listTests(cmd *cobra.Command, args []string) error {
}
tests = runner.ConvertTraceTestsToRunnerTests(all)
} else {
_ = config.Load("")
if err := config.Load("", cfgOverrideFile); err != nil {
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
return fmt.Errorf("failed to load config: %w", err)
}
}
cfg, getConfigErr := config.Get()

selected := traceDir
Expand Down
7 changes: 4 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import (
)

var (
cfgFile string
debug bool
showVersion bool
cfgFile string
cfgOverrideFile string
debug bool
showVersion bool

// Cleanup infrastructure
cleanupFuncs []func()
Expand Down
6 changes: 5 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ func runTests(cmd *cobra.Command, args []string) error {
executor := runner.NewExecutor()
executor.SetDebug(debug)

_ = config.Load(cfgFile)
if err := config.Load(cfgFile, cfgOverrideFile); err != nil {
if cfgOverrideFile != "" || os.Getenv("TUSK_CONFIG_OVERRIDE") != "" {
return fmt.Errorf("failed to load config: %w", err)
}
}
cfg, getConfigErr := config.Get()
if getConfigErr == nil && cfg.TestExecution.Concurrency > 0 {
executor.SetConcurrency(cfg.TestExecution.Concurrency)
Expand Down
47 changes: 45 additions & 2 deletions docs/drift/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,57 @@

This document lists all configuration options, defaults, environment overrides, and guidance. See [`docs/architecture.md`](architecture.md) for the end‑to‑end flow.

Where the CLI reads config from:
Where the CLI reads config from (highest precedence first):

1. CLI flags (e.g., `--concurrency`, `--results-dir`, `--enable-service-logs`). See `--help` for each command for more details.
2. Environment variables (prefix `TUSK_`)
3. Config file (auto-discovered): `.tusk/config.yaml`, `.tusk/config.yml`, `tusk.yaml`, `tusk.yml`, or `~/.tusk/config.yaml`
3. Config override file via `--config-override` flag or `TUSK_CONFIG_OVERRIDE` env var (flag takes precedence)
4. Base config file (auto-discovered): `.tusk/config.yaml`, `.tusk/config.yml`, `tusk.yaml`, `tusk.yml`, or `~/.tusk/config.yaml`

**✨ Run `tusk drift setup` in your service root directory to start an agent automatically create a config file based on your service.**

## Config Overrides

You can provide a config override file that is merged on top of the base config. Only the fields you specify in the override file are changed; everything else is preserved from the base config.

This is useful for local-only settings like disabling span export or changing the sampling mode without modifying the shared config file.

**Using the `--config-override` flag:**
```bash
tusk drift run --config-override .tusk/local-config.yaml
tusk drift list --config-override .tusk/local-config.yaml
```

**Using the `TUSK_CONFIG_OVERRIDE` env var:**
```bash
TUSK_CONFIG_OVERRIDE=.tusk/local-config.yaml tusk drift run
```

The `--config-override` flag takes precedence over the `TUSK_CONFIG_OVERRIDE` env var. Both are overridden by individual env vars (e.g., `TUSK_RECORDING_SAMPLING_MODE`).

Example override file:

```yaml
recording:
sampling:
mode: fixed
base_rate: 1.0
export_spans: false
enable_env_var_recording: false
```

### Recording Environment Variable Overrides

These env vars override the corresponding config keys regardless of what's in the config files:

| Env var | Config key |
|---------|-----------|
| `TUSK_RECORDING_SAMPLING_MODE` | `recording.sampling.mode` |
| `TUSK_RECORDING_SAMPLING_RATE` | `recording.sampling.base_rate` (+ legacy `recording.sampling_rate`) |
| `TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS` | `recording.sampling.log_transitions` |
| `TUSK_RECORDING_EXPORT_SPANS` | `recording.export_spans` |
| `TUSK_ENABLE_ENV_VAR_RECORDING` | `recording.enable_env_var_recording` |

## Service

<table>
Expand Down
36 changes: 34 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,14 @@ type CoverageConfig struct {
StripPathPrefix string `koanf:"strip_path_prefix"`
}

// Load loads the config file and applies environment overrides.
// Load loads the config file, applies overrides, and applies environment overrides.
// This function is idempotent - calling it multiple times will only load once.
func Load(configFile string) error {
//
// Config precedence (highest wins):
// 1. Environment variables (TUSK_*)
// 2. Override file (from --config-override flag or TUSK_CONFIG_OVERRIDE env var)
// 3. Base config file (.tusk/config.yaml)
func Load(configFile string, overrideFiles ...string) error {
loadMutex.Lock()
defer loadMutex.Unlock()

Expand All @@ -158,6 +163,30 @@ func Load(configFile string) error {
log.Debug("No config file found, using defaults and environment variables")
}

// Determine override file: --config-override flag takes precedence over TUSK_CONFIG_OVERRIDE env var.
// The env var fallback is only checked when the caller explicitly passes the overrideFiles arg
// (even if empty). Callers like ValidateConfigFile that pass no variadic arg won't trigger env
// var lookup, preventing TUSK_CONFIG_OVERRIDE from interfering with validation.
var overridePath string
if len(overrideFiles) > 0 {
overridePath = overrideFiles[0]
if overridePath == "" {
overridePath = os.Getenv("TUSK_CONFIG_OVERRIDE")
}
}

if overridePath != "" {
if _, err := os.Stat(overridePath); err == nil { // #nosec G703 -- path from trusted flag/env var
if err := k.Load(file.Provider(overridePath), yaml.Parser()); err != nil {
return fmt.Errorf("error loading config override file %s: %w", overridePath, err)
}
log.Debug("Config override file loaded", "file", overridePath)
configFileFound = true
} else {
return fmt.Errorf("config override file not found: %s", overridePath)
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Support environment variable overrides for specific config keys
envOverrides := map[string]string{
"TUSK_TRACES_DIR": "traces.dir",
Expand All @@ -167,6 +196,9 @@ func Load(configFile string) error {
"TUSK_RESULTS_DIR": "results.dir",
"TUSK_RECORDING_SAMPLING_RATE": "recording.sampling_rate",
"TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS": "recording.sampling.log_transitions",
"TUSK_RECORDING_SAMPLING_MODE": "recording.sampling.mode",
"TUSK_RECORDING_EXPORT_SPANS": "recording.export_spans",
"TUSK_ENABLE_ENV_VAR_RECORDING": "recording.enable_env_var_recording",
}

for envKey, configKey := range envOverrides {
Expand Down
Loading
Loading