fix: accept []string and []int in matrix refs (#2956)

Co-authored-by: no-hup <shauryaj.finance@gmail.com>
This commit is contained in:
shaurya
2026-08-10 23:17:58 +05:30
committed by GitHub
parent d4fd591dd3
commit 85aca587b1
5 changed files with 87 additions and 5 deletions

View File

@@ -929,6 +929,7 @@ func TestForCmds(t *testing.T) {
{name: "loop-explicit"}, {name: "loop-explicit"},
{name: "loop-matrix"}, {name: "loop-matrix"},
{name: "loop-matrix-ref"}, {name: "loop-matrix-ref"},
{name: "loop-matrix-ref-computed"},
{ {
name: "loop-matrix-ref-error", name: "loop-matrix-ref-error",
wantErr: true, wantErr: true,

View File

@@ -4,6 +4,7 @@ vars:
OS_VAR: ["windows", "linux", "darwin"] OS_VAR: ["windows", "linux", "darwin"]
ARCH_VAR: ["amd64", "arm64"] ARCH_VAR: ["amd64", "arm64"]
NOT_A_LIST: "not a list" NOT_A_LIST: "not a list"
OS_CSV: "windows,linux,darwin"
tasks: tasks:
# Loop over a list of values # Loop over a list of values
@@ -30,6 +31,16 @@ tasks:
ref: .ARCH_VAR ref: .ARCH_VAR
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref-computed:
cmds:
- for:
matrix:
OS:
ref: 'splitList "," .OS_CSV'
ARCH:
ref: .ARCH_VAR
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
loop-matrix-ref-error: loop-matrix-ref-error:
cmds: cmds:
- for: - for:

View File

@@ -0,0 +1,6 @@
windows/amd64
windows/arm64
linux/amd64
linux/arm64
darwin/amd64
darwin/arm64

View File

@@ -355,6 +355,22 @@ func asAnySlice[T any](slice []T) []any {
return ret return ret
} }
// resolvedAsAnySlice converts a value resolved from a reference into a []any.
// A reference does not always resolve to a []any: lists declared in a Taskfile
// do, but template functions such as `keys` and `splitList` return a []string.
// The accepted types mirror the list types itemsFromFor already supports.
func resolvedAsAnySlice(v any) ([]any, bool) {
switch value := v.(type) {
case []any:
return value, true
case []string:
return asAnySlice(value), true
case []int:
return asAnySlice(value), true
}
return nil, false
}
func itemsFromFor( func itemsFromFor(
f *ast.For, f *ast.For,
dir string, dir string,
@@ -476,12 +492,11 @@ func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) (*ast.Matrix,
if cache.Err() != nil { if cache.Err() != nil {
return nil, cache.Err() return nil, cache.Err()
} }
switch value := v.(type) { value, ok := resolvedAsAnySlice(v)
case []any: if !ok {
row.Value = value
default:
return nil, fmt.Errorf("matrix reference %q must resolve to a list", row.Ref) return nil, fmt.Errorf("matrix reference %q must resolve to a list", row.Ref)
} }
row.Value = value
} }
} }
return resolved, nil return resolved, nil
@@ -499,7 +514,7 @@ func resolveEnumRefs(requires *ast.Requires, cache *templater.Cache) error {
if cache.Err() != nil { if cache.Err() != nil {
return cache.Err() return cache.Err()
} }
arr, ok := resolved.([]any) arr, ok := resolvedAsAnySlice(resolved)
if !ok { if !ok {
return fmt.Errorf("enum reference %q must resolve to a list", v.Enum.Ref) return fmt.Errorf("enum reference %q must resolve to a list", v.Enum.Ref)
} }

View File

@@ -43,3 +43,52 @@ func TestResolveMatrixRefsDoesNotMutateInput(t *testing.T) {
require.Nil(t, orig.Value, "input matrix was mutated: Ref rows must be resolved into a copy") require.Nil(t, orig.Value, "input matrix was mutated: Ref rows must be resolved into a copy")
require.Equal(t, ".ARCH_VAR", orig.Ref, "input matrix Ref was altered") require.Equal(t, ".ARCH_VAR", orig.Ref, "input matrix Ref was altered")
} }
// TestResolveMatrixRefsListTypes is a regression test for #2544. A `ref:` is
// evaluated as a template expression, so it does not always resolve to a
// []any: a list declared in a Taskfile does, but template functions such as
// `keys` and `splitList` return a []string. Resolving a ref used to type
// assert []any, so those references failed with "must resolve to a list" even
// though the value was a list. The accepted types mirror the ones
// itemsFromFor supports for `for: var:`.
func TestResolveMatrixRefsListTypes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value any
want []any
wantErr bool
}{
{name: "any slice", value: []any{"amd64", "arm64"}, want: []any{"amd64", "arm64"}},
{name: "string slice", value: []string{"amd64", "arm64"}, want: []any{"amd64", "arm64"}},
{name: "int slice", value: []int{1, 2}, want: []any{1, 2}},
{name: "string", value: "not a list", wantErr: true},
{name: "map", value: map[string]any{"key": "value"}, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
matrix := ast.NewMatrix(
&ast.MatrixElement{Key: "ARCH", Value: &ast.MatrixRow{Ref: ".ARCH_VAR"}},
)
vars := ast.NewVars()
vars.Set("ARCH_VAR", ast.Var{Value: test.value})
cache := &templater.Cache{Vars: vars}
resolved, err := resolveMatrixRefs(matrix, cache)
if test.wantErr {
require.ErrorContains(t, err, `matrix reference ".ARCH_VAR" must resolve to a list`)
return
}
require.NoError(t, err)
row, ok := resolved.Get("ARCH")
require.True(t, ok, "ARCH row missing from resolved matrix")
require.Equal(t, test.want, row.Value)
})
}
}