From d880f1b927f5c7a3289ae413c2ffc4004e935fce Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 19 Feb 2026 10:07:05 -0500 Subject: [PATCH 1/2] Support non-string types in azd env config set (#6701) Add JSON type auto-detection to azd env config set so that bool, number, array, and object values are stored with their correct types in config.json instead of always being stored as strings. This fixes the issue where azd provision would re-prompt for values like testBool=true or testStrings=["one","two"] because they were stored as string types rather than their native JSON types. The parseConfigValue helper uses json.Unmarshal to detect types and falls back to string for unrecognized values. Users can force string type by JSON-quoting the value (e.g. '"true"'). Null is preserved as string. Fixes #6701 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/env.go | 23 ++++++- cli/azd/cmd/env_config_test.go | 112 ++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 2d2163f75c6..8f028091868 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" @@ -1503,7 +1504,9 @@ func newEnvConfigSetCmd() *cobra.Command { Long: "Sets a configuration value in the environment's config.json file.", 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"}'`, } } @@ -1567,7 +1570,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 +1582,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..2aa88e8e63b 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: "SetQuotedStringAsBool", + 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) } From 768115bbe51ee8ac7ce4e5294c17d8244ae01dec Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 19 Feb 2026 11:49:04 -0500 Subject: [PATCH 2/2] address PR review: improve help text and rename test - Update Long description with type parsing rules and examples - Rename misleading test to TestEnvConfigMultipleOperations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/env.go | 12 +++++++++--- cli/azd/cmd/env_config_test.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 8f028091868..8997bea8d04 100644 --- a/cli/azd/cmd/env.go +++ b/cli/azd/cmd/env.go @@ -1501,12 +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.count 42 -$ azd env config set infra.parameters.tags '{"env":"dev"}'`, +$ azd env config set infra.parameters.tags '{"env":"dev"}' +$ azd env config set myapp.port '"8080"'`, } } diff --git a/cli/azd/cmd/env_config_test.go b/cli/azd/cmd/env_config_test.go index 2aa88e8e63b..197e78ac1a3 100644 --- a/cli/azd/cmd/env_config_test.go +++ b/cli/azd/cmd/env_config_test.go @@ -341,7 +341,7 @@ func TestEnvConfigSet(t *testing.T) { expectError: false, }, { - name: "SetQuotedStringAsBool", + name: "SetQuotedJsonForcesStringType", initialConfig: map[string]any{}, path: "app.flag", value: `"true"`,