From ab28ecde88ddcb907fb44c634d2721ef3318030a Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Thu, 26 Feb 2026 14:34:30 +0100 Subject: [PATCH] fix(cli): reuse multi-select parameter values on workspace update (#22261) Fixes three bugs that caused `coder update` to always re-prompt for multi-select (`list(string)`) parameters instead of reusing previous build values: 1. **`isValidTemplateParameterOption` failed for multi-select values** (`cli/parameterresolver.go`): It compared the entire JSON array string (e.g. `["vim","emacs"]`) against individual option values, which never matched. Now parses the JSON array and validates each element separately. 2. **`RichParameter` ignored previous build value for multi-select** (`cli/cliui/parameter.go`): The `list(string)` branch always used the template's default value instead of the `defaultValue` argument (which carries the previous build's value). Now uses `defaultValue` when available, falling back to the template default. 3. **Pre-existing crash when `list(string)` has no default value** (`cli/cliui/parameter.go`): `json.Unmarshal` on an empty string caused `unexpected end of JSON input`. Now skips unmarshaling when the default source is empty. Fixes #19956 --- cli/cliui/parameter.go | 12 +++- cli/parameterresolver.go | 32 ++++++++-- cli/parameterresolver_internal_test.go | 85 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 cli/parameterresolver_internal_test.go diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go index 9b3e9e47fb..8fda0dd516 100644 --- a/cli/cliui/parameter.go +++ b/cli/cliui/parameter.go @@ -30,9 +30,15 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te _, _ = fmt.Fprint(inv.Stdout, "\033[1A") var defaults []string - err = json.Unmarshal([]byte(templateVersionParameter.DefaultValue), &defaults) - if err != nil { - return "", err + defaultSource := defaultValue + if defaultSource == "" { + defaultSource = templateVersionParameter.DefaultValue + } + if defaultSource != "" { + err = json.Unmarshal([]byte(defaultSource), &defaults) + if err != nil { + return "", err + } } values, err := RichMultiSelect(inv, RichMultiSelectOptions{ diff --git a/cli/parameterresolver.go b/cli/parameterresolver.go index 9308d245f8..d443741756 100644 --- a/cli/parameterresolver.go +++ b/cli/parameterresolver.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/json" "fmt" "strings" @@ -231,7 +232,7 @@ next: continue // immutables should not be passed to consecutive builds } - if len(tvp.Options) > 0 && !isValidTemplateParameterOption(buildParameter, tvp.Options) { + if len(tvp.Options) > 0 && !isValidTemplateParameterOption(buildParameter, *tvp) { continue // do not propagate invalid options } @@ -365,7 +366,7 @@ func (pr *ParameterResolver) isLastBuildParameterInvalidOption(templateVersionPa for _, buildParameter := range pr.lastBuildParameters { if buildParameter.Name == templateVersionParameter.Name { - return !isValidTemplateParameterOption(buildParameter, templateVersionParameter.Options) + return !isValidTemplateParameterOption(buildParameter, templateVersionParameter) } } return false @@ -389,8 +390,31 @@ func findWorkspaceBuildParameter(parameterName string, params []codersdk.Workspa return nil } -func isValidTemplateParameterOption(buildParameter codersdk.WorkspaceBuildParameter, options []codersdk.TemplateVersionParameterOption) bool { - for _, opt := range options { +func isValidTemplateParameterOption(buildParameter codersdk.WorkspaceBuildParameter, templateVersionParameter codersdk.TemplateVersionParameter) bool { + // Multi-select parameters store values as a JSON array (e.g. + // '["vim","emacs"]'), so we need to parse the array and validate + // each element individually against the allowed options. + if templateVersionParameter.Type == "list(string)" { + var values []string + if err := json.Unmarshal([]byte(buildParameter.Value), &values); err != nil { + return false + } + for _, v := range values { + found := false + for _, opt := range templateVersionParameter.Options { + if opt.Value == v { + found = true + break + } + } + if !found { + return false + } + } + return true + } + + for _, opt := range templateVersionParameter.Options { if opt.Value == buildParameter.Value { return true } diff --git a/cli/parameterresolver_internal_test.go b/cli/parameterresolver_internal_test.go new file mode 100644 index 0000000000..244627c58e --- /dev/null +++ b/cli/parameterresolver_internal_test.go @@ -0,0 +1,85 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/v2/codersdk" +) + +func TestIsValidTemplateParameterOption(t *testing.T) { + t.Parallel() + + options := []codersdk.TemplateVersionParameterOption{ + {Name: "Vim", Value: "vim"}, + {Name: "Emacs", Value: "emacs"}, + {Name: "VS Code", Value: "vscode"}, + } + + t.Run("SingleSelectValid", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editor", Value: "vim"} + tvp := codersdk.TemplateVersionParameter{ + Name: "editor", + Type: "string", + Options: options, + } + assert.True(t, isValidTemplateParameterOption(bp, tvp)) + }) + + t.Run("SingleSelectInvalid", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editor", Value: "notepad"} + tvp := codersdk.TemplateVersionParameter{ + Name: "editor", + Type: "string", + Options: options, + } + assert.False(t, isValidTemplateParameterOption(bp, tvp)) + }) + + t.Run("MultiSelectAllValid", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editors", Value: `["vim","emacs"]`} + tvp := codersdk.TemplateVersionParameter{ + Name: "editors", + Type: "list(string)", + Options: options, + } + assert.True(t, isValidTemplateParameterOption(bp, tvp)) + }) + + t.Run("MultiSelectOneInvalid", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editors", Value: `["vim","notepad"]`} + tvp := codersdk.TemplateVersionParameter{ + Name: "editors", + Type: "list(string)", + Options: options, + } + assert.False(t, isValidTemplateParameterOption(bp, tvp)) + }) + + t.Run("MultiSelectEmptyArray", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editors", Value: `[]`} + tvp := codersdk.TemplateVersionParameter{ + Name: "editors", + Type: "list(string)", + Options: options, + } + assert.True(t, isValidTemplateParameterOption(bp, tvp)) + }) + + t.Run("MultiSelectInvalidJSON", func(t *testing.T) { + t.Parallel() + bp := codersdk.WorkspaceBuildParameter{Name: "editors", Value: `not-json`} + tvp := codersdk.TemplateVersionParameter{ + Name: "editors", + Type: "list(string)", + Options: options, + } + assert.False(t, isValidTemplateParameterOption(bp, tvp)) + }) +}