Skip to content

Commit f2943f6

Browse files
michaelmcneesclaude
andcommitted
fix: address code review feedback
- Sort map keys in `env show` output for deterministic display - Move values file loading before DB open in run.go (fail-fast) - Detect env key collisions during uppercase normalization in values.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8eab6cf commit f2943f6

4 files changed

Lines changed: 60 additions & 27 deletions

File tree

packages/engine/internal/cli/env.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"sort"
56
"text/tabwriter"
67

78
"github.com/dvflw/mantle/internal/config"
@@ -117,14 +118,24 @@ func newEnvShowCommand() *cobra.Command {
117118
fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\n", env.Name)
118119
if len(env.Inputs) > 0 {
119120
fmt.Fprintln(cmd.OutOrStdout(), "\nInputs:")
120-
for k, v := range env.Inputs {
121-
fmt.Fprintf(cmd.OutOrStdout(), " %s: %v\n", k, v)
121+
keys := make([]string, 0, len(env.Inputs))
122+
for k := range env.Inputs {
123+
keys = append(keys, k)
124+
}
125+
sort.Strings(keys)
126+
for _, k := range keys {
127+
fmt.Fprintf(cmd.OutOrStdout(), " %s: %v\n", k, env.Inputs[k])
122128
}
123129
}
124130
if len(env.Env) > 0 {
125131
fmt.Fprintln(cmd.OutOrStdout(), "\nEnv:")
126-
for k, v := range env.Env {
127-
fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", k, v)
132+
keys := make([]string, 0, len(env.Env))
133+
for k := range env.Env {
134+
keys = append(keys, k)
135+
}
136+
sort.Strings(keys)
137+
for _, k := range keys {
138+
fmt.Fprintf(cmd.OutOrStdout(), " %s: %s\n", k, env.Env[k])
128139
}
129140
}
130141
return nil

packages/engine/internal/cli/run.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ func newRunCommand() *cobra.Command {
3838
return fmt.Errorf("config not loaded")
3939
}
4040

41+
// Parse --input flags early (no I/O needed).
42+
inputs := make(map[string]any)
43+
for _, kv := range inputFlags {
44+
key, value, ok := strings.Cut(kv, "=")
45+
if !ok {
46+
return fmt.Errorf("invalid input format %q — expected key=value", kv)
47+
}
48+
inputs[key] = value
49+
}
50+
51+
// Load values file early so we fail fast on bad input (before DB open).
52+
var valuesInputs map[string]any
53+
var valuesEnv map[string]string
54+
if valuesFile != "" {
55+
vals, valErr := workflow.LoadValues(valuesFile)
56+
if valErr != nil {
57+
return fmt.Errorf("loading values file: %w", valErr)
58+
}
59+
valuesInputs = vals.Inputs
60+
valuesEnv = vals.Env
61+
}
62+
4163
database, err := db.Open(cfg.Database)
4264
if err != nil {
4365
return fmt.Errorf("failed to connect to database: %w", err)
@@ -66,28 +88,6 @@ func newRunCommand() *cobra.Command {
6688
envEnvVars = storedEnv.Env
6789
}
6890

69-
// Parse --input flags.
70-
inputs := make(map[string]any)
71-
for _, kv := range inputFlags {
72-
key, value, ok := strings.Cut(kv, "=")
73-
if !ok {
74-
return fmt.Errorf("invalid input format %q — expected key=value", kv)
75-
}
76-
inputs[key] = value
77-
}
78-
79-
// Load values file and merge all input layers.
80-
var valuesInputs map[string]any
81-
var valuesEnv map[string]string
82-
if valuesFile != "" {
83-
vals, valErr := workflow.LoadValues(valuesFile)
84-
if valErr != nil {
85-
return fmt.Errorf("loading values file: %w", valErr)
86-
}
87-
valuesInputs = vals.Inputs
88-
valuesEnv = vals.Env
89-
}
90-
9191
// NOTE: MergeInputs does not apply workflow defaults (that's the engine's job
9292
// during execution). Pass nil for workflowInputs here — the engine handles it.
9393
mergedInputs := workflow.MergeInputs(nil, envInputs, valuesInputs, inputs)

packages/engine/internal/workflow/values.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,17 @@ func LoadValuesBytes(data []byte) (*Values, error) {
4545
}
4646

4747
// Normalize env keys to uppercase (matches config.go behavior).
48+
// Detect collisions where different-cased keys map to the same uppercase key.
4849
if len(vals.Env) > 0 {
4950
normalized := make(map[string]string, len(vals.Env))
51+
sources := make(map[string]string, len(vals.Env)) // uppercase key -> original key
5052
for k, v := range vals.Env {
51-
normalized[strings.ToUpper(k)] = v
53+
upper := strings.ToUpper(k)
54+
if orig, exists := sources[upper]; exists && orig != k {
55+
return nil, fmt.Errorf("env key collision: %q and %q both normalize to %q", orig, k, upper)
56+
}
57+
sources[upper] = k
58+
normalized[upper] = v
5259
}
5360
vals.Env = normalized
5461
}

packages/engine/internal/workflow/values_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,21 @@ env:
169169
}
170170
}
171171

172+
func TestLoadValues_EnvKeyCollision(t *testing.T) {
173+
path := writeValuesFile(t, `
174+
env:
175+
api_key: lowercase_value
176+
API_KEY: uppercase_value
177+
`)
178+
_, err := LoadValues(path)
179+
if err == nil {
180+
t.Fatal("expected error for env key collision")
181+
}
182+
if !strings.Contains(err.Error(), "collision") {
183+
t.Errorf("error = %q, want it to contain %q", err.Error(), "collision")
184+
}
185+
}
186+
172187
func TestLoadValues_EmptyFile(t *testing.T) {
173188
path := writeValuesFile(t, "")
174189

0 commit comments

Comments
 (0)