From 247d2a38d0efb752abc319c76429847d06ffed19 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 9 Mar 2026 21:07:55 -0700 Subject: [PATCH 1/6] fix(compiler): expose MATCH while evaluating include vars --- compiler.go | 7 +++++++ task_test.go | 17 +++++++++++++++++ .../includes_wildcard_vars/Taskfile.stack.yml | 6 ++++++ testdata/includes_wildcard_vars/Taskfile.yml | 7 +++++++ 4 files changed, 37 insertions(+) create mode 100644 testdata/includes_wildcard_vars/Taskfile.stack.yml create mode 100644 testdata/includes_wildcard_vars/Taskfile.yml diff --git a/compiler.go b/compiler.go index 311fd58423..68a586bca6 100644 --- a/compiler.go +++ b/compiler.go @@ -115,6 +115,13 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (* } } if t != nil { + if call != nil { + if match, ok := call.Vars.Get("MATCH"); ok { + if err := rangeFunc("MATCH", match); err != nil { + return nil, err + } + } + } for k, v := range t.IncludeVars.All() { if err := rangeFunc(k, v); err != nil { return nil, err diff --git a/task_test.go b/task_test.go index 9d54af9740..43b6dc4d53 100644 --- a/task_test.go +++ b/task_test.go @@ -1271,6 +1271,23 @@ func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot } } +func TestIncludeWildcardVarsExposeMatch(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + e := task.NewExecutor( + task.WithDir("testdata/includes_wildcard_vars"), + task.WithSilent(true), + task.WithStdout(&buff), + task.WithStderr(&buff), + ) + require.NoError(t, e.Setup()) + + err := e.Run(t.Context(), &task.Call{Task: "stack:prod:show"}) + require.NoError(t, err) + assert.Equal(t, "prod\n", buff.String()) +} + func TestIncludesWithExclude(t *testing.T) { t.Parallel() diff --git a/testdata/includes_wildcard_vars/Taskfile.stack.yml b/testdata/includes_wildcard_vars/Taskfile.stack.yml new file mode 100644 index 0000000000..19994814c5 --- /dev/null +++ b/testdata/includes_wildcard_vars/Taskfile.stack.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + show: + cmds: + - echo '{{.ENV}}' diff --git a/testdata/includes_wildcard_vars/Taskfile.yml b/testdata/includes_wildcard_vars/Taskfile.yml new file mode 100644 index 0000000000..5ed3e073f5 --- /dev/null +++ b/testdata/includes_wildcard_vars/Taskfile.yml @@ -0,0 +1,7 @@ +version: '3' + +includes: + 'stack:*': + taskfile: ./Taskfile.stack.yml + vars: + ENV: '{{index .MATCH 0}}' From a0f16c8508c4adbfe3b13e643070af5954d45e3d Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 08:47:43 -0700 Subject: [PATCH 2/6] fix(task): expose wildcard MATCH to include vars outside compiler --- compiler.go | 7 ------- task.go | 13 ++++++++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/compiler.go b/compiler.go index 68a586bca6..311fd58423 100644 --- a/compiler.go +++ b/compiler.go @@ -115,13 +115,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (* } } if t != nil { - if call != nil { - if match, ok := call.Vars.Get("MATCH"); ok { - if err := rangeFunc("MATCH", match); err != nil { - return nil, err - } - } - } for k, v := range t.IncludeVars.All() { if err := rangeFunc(k, v); err != nil { return nil, err diff --git a/task.go b/task.go index 54cda92762..6d30aa02f9 100644 --- a/task.go +++ b/task.go @@ -529,7 +529,18 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { if call.Vars == nil { call.Vars = ast.NewVars() } - call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards}) + match := matchingTasks[0].Wildcards + call.Vars.Set("MATCH", ast.Var{Value: match}) + + if len(match) > 0 { + taskCopy := matchingTasks[0].Task.DeepCopy() + if taskCopy.IncludeVars == nil { + taskCopy.IncludeVars = ast.NewVars() + } + taskCopy.IncludeVars.Set("MATCH", ast.Var{Value: match}) + return taskCopy, nil + } + return matchingTasks[0].Task, nil } From fee6a920c29dcba731acac7e47346e1603883b57 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 08:53:35 -0700 Subject: [PATCH 3/6] fix(task): preserve include MATCH when resolving wildcard tasks --- task.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/task.go b/task.go index 6d30aa02f9..6dd9c727c7 100644 --- a/task.go +++ b/task.go @@ -526,18 +526,32 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { } if len(matchingTasks) > 0 { - if call.Vars == nil { - call.Vars = ast.NewVars() - } match := matchingTasks[0].Wildcards - call.Vars.Set("MATCH", ast.Var{Value: match}) + + var ( + includeMatch ast.Var + hasMatchVar bool + ) + + if call.Vars != nil { + includeMatch, hasMatchVar = call.Vars.Get("MATCH") + } if len(match) > 0 { + if call.Vars == nil { + call.Vars = ast.NewVars() + } + includeMatch = ast.Var{Value: match} + hasMatchVar = true + call.Vars.Set("MATCH", includeMatch) + } + + if hasMatchVar { taskCopy := matchingTasks[0].Task.DeepCopy() if taskCopy.IncludeVars == nil { taskCopy.IncludeVars = ast.NewVars() } - taskCopy.IncludeVars.Set("MATCH", ast.Var{Value: match}) + taskCopy.IncludeVars.Set("MATCH", includeMatch) return taskCopy, nil } From 8e1c24a61cc0f95f07f69bc5f6ee8d673a5779e4 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 08:56:45 -0700 Subject: [PATCH 4/6] fix(task): default MATCH when absent without clobbering include MATCH --- task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/task.go b/task.go index 6dd9c727c7..ed9098b299 100644 --- a/task.go +++ b/task.go @@ -537,7 +537,7 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { includeMatch, hasMatchVar = call.Vars.Get("MATCH") } - if len(match) > 0 { + if len(match) > 0 || !hasMatchVar { if call.Vars == nil { call.Vars = ast.NewVars() } From d9c6667319fbebba69c7ad993ca589157c22ea78 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 09:02:07 -0700 Subject: [PATCH 5/6] fix(task): seed include vars with MATCH before template eval --- task.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/task.go b/task.go index ed9098b299..6ee8f8f8cf 100644 --- a/task.go +++ b/task.go @@ -548,10 +548,13 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { if hasMatchVar { taskCopy := matchingTasks[0].Task.DeepCopy() - if taskCopy.IncludeVars == nil { - taskCopy.IncludeVars = ast.NewVars() + matchFirstIncludeVars := ast.NewVars() + matchFirstIncludeVars.Set("MATCH", includeMatch) + if taskCopy.IncludeVars != nil { + matchFirstIncludeVars.Merge(taskCopy.IncludeVars, nil) } - taskCopy.IncludeVars.Set("MATCH", includeMatch) + matchFirstIncludeVars.Set("MATCH", includeMatch) + taskCopy.IncludeVars = matchFirstIncludeVars return taskCopy, nil } From 647237fbec6926c97320867ada7d88363fd71685 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 09:04:30 -0700 Subject: [PATCH 6/6] fix(task): normalize empty MATCH to non-nil slice --- task.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/task.go b/task.go index 6ee8f8f8cf..2c7eb06d5c 100644 --- a/task.go +++ b/task.go @@ -527,6 +527,9 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { if len(matchingTasks) > 0 { match := matchingTasks[0].Wildcards + if match == nil { + match = []string{} + } var ( includeMatch ast.Var @@ -535,6 +538,12 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) { if call.Vars != nil { includeMatch, hasMatchVar = call.Vars.Get("MATCH") + if hasMatchVar { + if existingMatch, ok := includeMatch.Value.([]string); ok && existingMatch == nil { + includeMatch = ast.Var{Value: []string{}} + call.Vars.Set("MATCH", includeMatch) + } + } } if len(match) > 0 || !hasMatchVar {