From 9c865d5e2944aed84d2da4ae36bd83c8c8ad4968 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 25 Jan 2026 19:40:29 +0100 Subject: [PATCH 1/2] fix: evaluate task-level if condition after resolving dynamic variables --- executor_test.go | 4 ++++ task.go | 23 ++++++++++--------- testdata/if/Taskfile.yml | 18 +++++++++++++++ .../TestIf-task-if-dynamic-false.golden | 2 ++ .../TestIf-task-if-dynamic-true.golden | 1 + 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 testdata/if/testdata/TestIf-task-if-dynamic-false.golden create mode 100644 testdata/if/testdata/TestIf-task-if-dynamic-true.golden diff --git a/executor_test.go b/executor_test.go index d810e652f0..6e3ff3e1ec 100644 --- a/executor_test.go +++ b/executor_test.go @@ -1160,6 +1160,10 @@ func TestIf(t *testing.T) { // For loop with if {name: "if-in-for-loop", task: "if-in-for-loop", verbose: true}, + + // Task-level if with dynamic variable + {name: "task-if-dynamic-true", task: "task-if-dynamic-true"}, + {name: "task-if-dynamic-false", task: "task-if-dynamic-false", verbose: true}, } for _, test := range tests { diff --git a/task.go b/task.go index 0184793c91..5456115fc4 100644 --- a/task.go +++ b/task.go @@ -148,17 +148,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { return nil } - if strings.TrimSpace(t.If) != "" { - if err := execext.RunCommand(ctx, &execext.RunCommandOptions{ - Command: t.If, - Dir: t.Dir, - Env: env.Get(t), - }); err != nil { - e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task) - return nil - } - } - // Prompt for missing required vars (just-in-time for sequential task calls) prompted, err := e.promptTaskVars(t, call) if err != nil { @@ -185,6 +174,18 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { return err } + // Check if condition after CompiledTask so dynamic variables are resolved + if strings.TrimSpace(t.If) != "" { + if err := execext.RunCommand(ctx, &execext.RunCommandOptions{ + Command: t.If, + Dir: t.Dir, + Env: env.Get(t), + }); err != nil { + e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task) + return nil + } + } + if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { return &errors.TaskCalledTooManyTimesError{ TaskName: t.Task, diff --git a/testdata/if/Taskfile.yml b/testdata/if/Taskfile.yml index 316b732ff0..72b72ed982 100644 --- a/testdata/if/Taskfile.yml +++ b/testdata/if/Taskfile.yml @@ -158,3 +158,21 @@ tasks: if: '{{ eq .ENV "dev" }}' cmds: - echo "should not appear" + + # Task-level if with dynamic variable (condition met) + task-if-dynamic-true: + vars: + ENABLE_FEATURE: + sh: 'echo "true"' + if: '{{ eq .ENABLE_FEATURE "true" }}' + cmds: + - echo "dynamic feature enabled" + + # Task-level if with dynamic variable (condition not met) + task-if-dynamic-false: + vars: + ENABLE_FEATURE: + sh: 'echo "false"' + if: '{{ eq .ENABLE_FEATURE "true" }}' + cmds: + - echo "should not appear" diff --git a/testdata/if/testdata/TestIf-task-if-dynamic-false.golden b/testdata/if/testdata/TestIf-task-if-dynamic-false.golden new file mode 100644 index 0000000000..f17ef42029 --- /dev/null +++ b/testdata/if/testdata/TestIf-task-if-dynamic-false.golden @@ -0,0 +1,2 @@ +task: dynamic variable: "echo \"false\"" result: "false" +task: if condition not met - skipped: "task-if-dynamic-false" diff --git a/testdata/if/testdata/TestIf-task-if-dynamic-true.golden b/testdata/if/testdata/TestIf-task-if-dynamic-true.golden new file mode 100644 index 0000000000..4d1ec95b6a --- /dev/null +++ b/testdata/if/testdata/TestIf-task-if-dynamic-true.golden @@ -0,0 +1 @@ +dynamic feature enabled From a4bfc16bc5ef7b4b22d8916c1b39eb9c71317f32 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 25 Jan 2026 20:39:01 +0100 Subject: [PATCH 2/2] fix: skip prompting for vars when task if condition fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the prompt for required variables AFTER the if condition check. This avoids asking the user for input when the task won't run anyway. The order in RunTask() is now: 1. FastCompiledTask 2. Check required vars early (non-interactive mode only) 3. CompiledTask (resolve dynamic vars) 4. Check if condition → exit early if false 5. Prompt for missing vars (only if task will run) 6. Validate required vars --- task.go | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/task.go b/task.go index 5456115fc4..54cda92762 100644 --- a/task.go +++ b/task.go @@ -148,32 +148,19 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { return nil } - // Prompt for missing required vars (just-in-time for sequential task calls) - prompted, err := e.promptTaskVars(t, call) - if err != nil { - return err - } - if prompted { - // Recompile with the new vars - t, err = e.FastCompiledTask(call) - if err != nil { + // Check required vars early (before template compilation) if we can't prompt. + // This gives a clear "missing required variables" error instead of a template error. + if !e.canPrompt() { + if err := e.areTaskRequiredVarsSet(t); err != nil { return err } } - if err := e.areTaskRequiredVarsSet(t); err != nil { - return err - } - t, err = e.CompiledTask(call) if err != nil { return err } - if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil { - return err - } - // Check if condition after CompiledTask so dynamic variables are resolved if strings.TrimSpace(t.If) != "" { if err := execext.RunCommand(ctx, &execext.RunCommandOptions{ @@ -186,6 +173,27 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { } } + // Prompt for missing required vars after if check (avoid prompting if task won't run) + prompted, err := e.promptTaskVars(t, call) + if err != nil { + return err + } + if prompted { + // Recompile with the new vars + t, err = e.FastCompiledTask(call) + if err != nil { + return err + } + } + + if err := e.areTaskRequiredVarsSet(t); err != nil { + return err + } + + if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil { + return err + } + if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { return &errors.TaskCalledTooManyTimesError{ TaskName: t.Task,