Compare commits

..

4 Commits

Author SHA1 Message Date
Andrey Nering
09e7247d05 v3.48.0 2026-01-26 09:26:23 -03:00
Andrey Nering
502f24a2ad docs(changelog): add entry for #2658 and #2660 2026-01-26 09:24:26 -03:00
Valentin Maerten
f09f31c6d5 fix: skip prompting for vars when task if condition fails
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
2026-01-26 09:21:09 -03:00
Valentin Maerten
5a78808caa fix: evaluate task-level if condition after resolving dynamic variables 2026-01-26 09:21:09 -03:00
11 changed files with 58 additions and 69 deletions

View File

@@ -1,7 +1,9 @@
# Changelog
## Unreleased
## v3.48.0 - 2026-01-26
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
@trulede).

View File

@@ -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 {

View File

@@ -1 +1 @@
3.47.0
3.48.0

37
task.go
View File

@@ -148,6 +148,20 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return 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
}
}
t, err = e.CompiledTask(call)
if 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{
Command: t.If,
@@ -159,7 +173,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
}
}
// Prompt for missing required vars (just-in-time for sequential task calls)
// 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
@@ -176,11 +190,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return err
}
t, err = e.CompiledTask(call)
if err != nil {
return err
}
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err
}
@@ -365,22 +374,6 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
}
}
// Handle ask attached to command (y/n confirmation)
if cmd.Ask != "" && !e.Dry {
if e.AssumeYes {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s [assuming yes]\n", t.Name(), cmd.Ask)
} else {
if err := e.Logger.Prompt(logger.Yellow, cmd.Ask, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] ask declined - skipped\n", t.Name())
return nil
} else if err != nil {
return err
}
}
}
switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()

View File

@@ -20,7 +20,6 @@ type Cmd struct {
IgnoreError bool
Defer bool
Platforms []*Platform
Ask string
}
func (c *Cmd) DeepCopy() *Cmd {
@@ -39,7 +38,6 @@ func (c *Cmd) DeepCopy() *Cmd {
IgnoreError: c.IgnoreError,
Defer: c.Defer,
Platforms: deepcopy.Slice(c.Platforms),
Ask: c.Ask,
}
}
@@ -67,7 +65,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
IgnoreError bool `yaml:"ignore_error"`
Defer *Defer
Platforms []*Platform
Ask string
}
if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -101,7 +98,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
c.Ask = cmdStruct.Ask
return nil
}
@@ -115,7 +111,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.Shopt = cmdStruct.Shopt
c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms
c.Ask = cmdStruct.Ask
return nil
}

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
task: dynamic variable: "echo \"false\"" result: "false"
task: if condition not met - skipped: "task-if-dynamic-false"

View File

@@ -0,0 +1 @@
dynamic feature enabled

View File

@@ -7,6 +7,20 @@ outline: deep
::: v-pre
## v3.48.0 - 2026-01-26
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
@trulede).
- Included Taskfiles with `silent: true` now properly propagate silence to their
tasks, while still allowing individual tasks to override with `silent: false`
(#2640, #1319 by @trulede).
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
(#2537, #2242 by @vmaerten).
## v3.47.0 - 2026-01-24
- Fixed remote git Taskfiles: cloning now works without explicit ref, and

View File

@@ -741,8 +741,6 @@ tasks:
platforms: [linux, darwin]
set: [errexit]
shopt: [globstar]
if: '[ "$CI" = "true" ]'
ask: "Run this command?"
```
### Task References
@@ -859,36 +857,6 @@ tasks:
if: '[ "{{.ITEM}}" != "b" ]'
```
### Command Confirmations
Use `ask` to request user confirmation before executing a command. If the
user declines (answers "n" or "no"), the command is skipped but the task
continues.
```yaml
tasks:
deploy:
cmds:
- cmd: echo "Deploying to production..."
ask: "Deploy to production?"
- cmd: echo "Updating database..."
ask: "Run database migrations?"
- echo "Done!" # No ask, always runs
```
| Flag | Behavior |
|------|----------|
| (none) | Asks user for y/n confirmation |
| `--yes` | Auto-confirms all asks |
| `--dry` | Shows commands without asking |
:::note
This is different from the task-level `prompt:` which cancels the entire task
if declined. Command-level `ask:` only skips the individual command.
:::
## Shell Options
### Set Options

View File

@@ -340,10 +340,6 @@
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
},
"ask": {
"description": "A y/n confirmation shown before executing this task call. If declined, the task call is skipped.",
"type": "string"
}
},
"additionalProperties": false,
@@ -385,10 +381,6 @@
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
},
"ask": {
"description": "A y/n confirmation shown before executing this command. If declined, the command is skipped.",
"type": "string"
}
},
"additionalProperties": false,