fix(compiler): CLI vars have highest priority in scoped mode

In scoped mode, CLI vars (e.g., `task foo VAR=value`) now correctly
override task-level vars. This is achieved by:

1. Adding a `CLIVars` field to the Compiler struct
2. Storing CLI globals in this field after parsing
3. Applying CLI vars last in scoped mode to ensure they override everything

The order of variable resolution in scoped mode is now:
1. OS env → {{.env.XXX}}
2. Root taskfile env → {{.env.XXX}}
3. Root taskfile vars → {{.VAR}}
4. Include taskfile env/vars (if applicable)
5. IncludeVars (vars passed via includes: section)
6. Task-level vars
7. CLI vars (highest priority)

Legacy mode behavior is unchanged.
This commit is contained in:
Valentin Maerten
2025-12-29 16:45:41 +01:00
parent edee501b6b
commit e05c9f7793
3 changed files with 50 additions and 21 deletions

View File

@@ -174,6 +174,8 @@ func run() error {
// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults
e.Taskfile.Vars.Merge(globals, nil)
// Store CLI vars for scoped mode where they need highest priority
e.Compiler.CLIVars = globals
// Then ReverseMerge special variables so they're available for templating
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)

View File

@@ -26,6 +26,7 @@ type Compiler struct {
TaskfileEnv *ast.Vars
TaskfileVars *ast.Vars
CLIVars *ast.Vars // CLI vars passed via command line (e.g., task foo VAR=value)
Graph *ast.TaskfileGraph
Logger *logger.Logger
@@ -210,38 +211,58 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
}
}
// Inject env namespace into result
result.Set("env", ast.Var{Value: envMap})
} else {
// Legacy behavior: use merged vars
for k, v := range c.TaskfileEnv.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range c.TaskfileVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
if t != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range t.IncludedTaskfileVars.All() {
// Apply task-level vars
if call != nil {
for k, v := range t.Vars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
}
// CLI vars have highest priority - applied last to override everything
for k, v := range c.CLIVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
// Inject env namespace into result
result.Set("env", ast.Var{Value: envMap})
return result, nil
}
// === LEGACY MODE ===
// Legacy behavior: use merged vars
for k, v := range c.TaskfileEnv.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range c.TaskfileVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
if t != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range t.IncludedTaskfileVars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
}
if t == nil || call == nil {
return result, nil
}
// Legacy order: CLI vars, then task vars (task vars override CLI)
for k, v := range call.Vars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err

View File

@@ -36,3 +36,9 @@ tasks:
# In scoped mode, {{.ROOT_ENV}} should be empty (env not at root)
# In legacy mode, {{.ROOT_ENV}} would have the value
- echo "ROOT_ENV_AT_ROOT={{.ROOT_ENV}}"
prout:
vars:
LOL: prout_from_root
cmds:
- echo "{{.LOL}}"