diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 2d2163f75c6..8997bea8d04 100644 --- a/cli/azd/cmd/env.go +++ b/cli/azd/cmd/env.go @@ -5,6 +5,7 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -1500,10 +1501,18 @@ func newEnvConfigSetCmd() *cobra.Command { return &cobra.Command{ Use: "set ", Short: "Sets a configuration value in the environment.", - Long: "Sets a configuration value in the environment's config.json file.", - Args: cobra.ExactArgs(2), + Long: `Sets a configuration value in the environment's config.json file. + +Values are automatically parsed as JSON types when possible. Booleans (true/false), +numbers (42, 3.14), arrays ([...]), and objects ({...}) are stored with their native +JSON types. Plain text values are stored as strings. To force a JSON-typed value to be +stored as a string, wrap it in JSON quotes (e.g. '"true"' or '"8080"').`, + Args: cobra.ExactArgs(2), Example: `$ azd env config set myapp.endpoint https://example.com -$ azd env config set myapp.debug true`, +$ azd env config set myapp.debug true +$ azd env config set myapp.count 42 +$ azd env config set infra.parameters.tags '{"env":"dev"}' +$ azd env config set myapp.port '"8080"'`, } } @@ -1567,7 +1576,7 @@ func (a *envConfigSetAction) Run(ctx context.Context) (*actions.ActionResult, er path := a.args[0] value := a.args[1] - err = env.Config.Set(path, value) + err = env.Config.Set(path, parseConfigValue(value)) if err != nil { return nil, fmt.Errorf("failed setting configuration value '%s' to '%s'. %w", path, value, err) } @@ -1579,6 +1588,22 @@ func (a *envConfigSetAction) Run(ctx context.Context) (*actions.ActionResult, er return nil, nil } +// parseConfigValue attempts to parse a string value as a JSON type (bool, number, array, object). +// If parsing fails or the result is null, the original string is returned. +// JSON-quoted strings (e.g. `"true"`) are returned as their unquoted value, +// allowing users to force string type for values that would otherwise parse as bool/number. +func parseConfigValue(s string) any { + var parsed any + if err := json.Unmarshal([]byte(s), &parsed); err != nil { + return s + } + // null should remain as the string "null", not a nil value + if parsed == nil { + return s + } + return parsed +} + // azd env config unset func newEnvConfigUnsetCmd() *cobra.Command { diff --git a/cli/azd/cmd/env_config_test.go b/cli/azd/cmd/env_config_test.go index c259c110c27..197e78ac1a3 100644 --- a/cli/azd/cmd/env_config_test.go +++ b/cli/azd/cmd/env_config_test.go @@ -227,7 +227,7 @@ func TestEnvConfigSet(t *testing.T) { expectedConfig: map[string]any{ "app": map[string]any{ "endpoint": "https://example.com", - "port": "8080", + "port": float64(8080), }, }, expectError: false, @@ -246,6 +246,112 @@ func TestEnvConfigSet(t *testing.T) { }, expectError: false, }, + { + name: "SetBoolValueTrue", + initialConfig: map[string]any{}, + path: "infra.parameters.testBool", + value: "true", + expectedConfig: map[string]any{ + "infra": map[string]any{ + "parameters": map[string]any{ + "testBool": true, + }, + }, + }, + expectError: false, + }, + { + name: "SetBoolValueFalse", + initialConfig: map[string]any{}, + path: "infra.parameters.testBool", + value: "false", + expectedConfig: map[string]any{ + "infra": map[string]any{ + "parameters": map[string]any{ + "testBool": false, + }, + }, + }, + expectError: false, + }, + { + name: "SetNumberValue", + initialConfig: map[string]any{}, + path: "infra.parameters.count", + value: "42", + expectedConfig: map[string]any{ + "infra": map[string]any{ + "parameters": map[string]any{ + "count": float64(42), + }, + }, + }, + expectError: false, + }, + { + name: "SetArrayValue", + initialConfig: map[string]any{}, + path: "infra.parameters.testStrings", + value: `["one", "two", "three"]`, + expectedConfig: map[string]any{ + "infra": map[string]any{ + "parameters": map[string]any{ + "testStrings": []any{"one", "two", "three"}, + }, + }, + }, + expectError: false, + }, + { + name: "SetObjectValue", + initialConfig: map[string]any{}, + path: "infra.parameters.tags", + value: `{"env":"dev","team":"platform"}`, + expectedConfig: map[string]any{ + "infra": map[string]any{ + "parameters": map[string]any{ + "tags": map[string]any{"env": "dev", "team": "platform"}, + }, + }, + }, + expectError: false, + }, + { + name: "SetPlainStringValue", + initialConfig: map[string]any{}, + path: "app.name", + value: "my-app", + expectedConfig: map[string]any{ + "app": map[string]any{ + "name": "my-app", + }, + }, + expectError: false, + }, + { + name: "SetNullAsString", + initialConfig: map[string]any{}, + path: "app.value", + value: "null", + expectedConfig: map[string]any{ + "app": map[string]any{ + "value": "null", + }, + }, + expectError: false, + }, + { + name: "SetQuotedJsonForcesStringType", + initialConfig: map[string]any{}, + path: "app.flag", + value: `"true"`, + expectedConfig: map[string]any{ + "app": map[string]any{ + "flag": "true", + }, + }, + expectError: false, + }, } for _, tt := range tests { @@ -552,7 +658,7 @@ func TestEnvConfigMultipleOperations(t *testing.T) { err = json.Unmarshal(buf.Bytes(), &result) require.NoError(t, err) require.Equal(t, "https://example.com", result["endpoint"]) - require.Equal(t, "8080", result["port"]) + require.Equal(t, float64(8080), result["port"]) // Unset one value unsetFlags := &envConfigUnsetFlags{} @@ -584,5 +690,5 @@ func TestEnvConfigMultipleOperations(t *testing.T) { var result2 map[string]any err = json.Unmarshal(buf.Bytes(), &result2) require.NoError(t, err) - require.Equal(t, map[string]any{"port": "8080"}, result2) + require.Equal(t, map[string]any{"port": float64(8080)}, result2) }