Description
After upgrading from Task v3.48.0 to v3.49.0, global variables defined with sh: no longer resolve correctly when a Taskfile has includes:. The sh: variable resolves to an empty string, causing downstream variables that reference it via templating to produce incorrect values.
This regression was introduced by #2632 (commit 2cdd7d3), which shipped in v3.49.0.
Root Cause
The added line in compiler.go (line 118):
c.TaskfileVars = templater.ReplaceVars(c.TaskfileVars, &templater.Cache{Vars: result})
This mutates c.TaskfileVars after sh: dynamic variables have already been resolved by rangeFunc (lines 112-116), destroying their cached results.
Step-by-step breakdown:
-
Lines 112-116 (rangeFunc): Iterates c.TaskfileVars. For sh: vars like DYNAMIC_VAR, it calls HandleDynamicVar() which executes the shell command and stores the resolved value in the result map. At this point, result["DYNAMIC_VAR"] = "hello" ✅
-
Line 118 (the bug): ReplaceVars re-processes the original, unmodified c.TaskfileVars. In ReplaceVarWithExtra (templater.go):
-
Any subsequent reference to DYNAMIC_VAR sees Value: nil → resolves to empty string.
Suggested Fix
The ReplaceVars call should only process variables that actually have Ref values set, leaving already-resolved sh: variables untouched:
for k, v := range c.TaskfileVars.All() {
if v.Ref != "" {
resolved := templater.ResolveRef(v.Ref, &templater.Cache{Vars: result})
c.TaskfileVars.Set(k, ast.Var{Value: resolved})
}
}
Minimal Reproducer
Taskfile.yml:
version: "3"
includes:
other:
taskfile: ./Other.yml
internal: true
vars:
DYNAMIC_VAR:
sh: echo "hello"
DERIVED_VAR: "{{.DYNAMIC_VAR}} world"
tasks:
default:
cmds:
- 'echo "DYNAMIC_VAR={{.DYNAMIC_VAR}}"'
- 'echo "DERIVED_VAR={{.DERIVED_VAR}}"'
silent: true
Other.yml:
version: "3"
tasks:
noop:
cmds:
- "true"
v3.48.0: DYNAMIC_VAR=hello, DERIVED_VAR=hello world
v3.49.0: DYNAMIC_VAR=, DERIVED_VAR= world
Environment
- Task version: v3.49.0 (works on v3.48.0)
- OS: Ubuntu 24.04 (linux/arm64, GitHub Actions runner)
- Installed via:
go-task/setup-task@v1.0.0 (no version pin, so it picked up v3.49.0 automatically)
Description
After upgrading from Task v3.48.0 to v3.49.0, global variables defined with
sh:no longer resolve correctly when a Taskfile hasincludes:. Thesh:variable resolves to an empty string, causing downstream variables that reference it via templating to produce incorrect values.This regression was introduced by #2632 (commit 2cdd7d3), which shipped in v3.49.0.
Root Cause
The added line in
compiler.go(line 118):This mutates
c.TaskfileVarsaftersh:dynamic variables have already been resolved byrangeFunc(lines 112-116), destroying their cached results.Step-by-step breakdown:
Lines 112-116 (
rangeFunc): Iteratesc.TaskfileVars. Forsh:vars likeDYNAMIC_VAR, it callsHandleDynamicVar()which executes the shell command and stores the resolved value in theresultmap. At this point,result["DYNAMIC_VAR"] = "hello"✅Line 118 (the bug):
ReplaceVarsre-processes the original, unmodifiedc.TaskfileVars. InReplaceVarWithExtra(templater.go):Ref != ""→ resolves the ref (the intended fix for Reference viaref:or via{{}}Templating of includedvars(global, not task) in(to) a Taskfilevarssection (global, not task) doesn't work. #2405) ✅Ref == ""(likesh:vars) → callsReplaceWithExtraon theShfield, which only does template substitution, not shell execution. Returns a newVar{Sh: "echo hello", Value: nil}— the resolved value is lost ❌Any subsequent reference to
DYNAMIC_VARseesValue: nil→ resolves to empty string.Suggested Fix
The
ReplaceVarscall should only process variables that actually haveRefvalues set, leaving already-resolvedsh:variables untouched:Minimal Reproducer
Taskfile.yml:Other.yml:v3.48.0:
DYNAMIC_VAR=hello,DERIVED_VAR=hello worldv3.49.0:
DYNAMIC_VAR=,DERIVED_VAR= worldEnvironment
go-task/setup-task@v1.0.0(no version pin, so it picked up v3.49.0 automatically)