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
This commit is contained in:
Kacper Sawicki
2026-02-26 14:34:30 +01:00
committed by GitHub
parent bef7eb9dcc
commit ab28ecde88
3 changed files with 122 additions and 7 deletions
+9 -3
View File
@@ -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{
+28 -4
View File
@@ -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
}
+85
View File
@@ -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))
})
}