mirror of
https://github.com/go-task/task.git
synced 2026-06-25 05:35:56 +00:00
Compare commits
4 Commits
feat/comma
...
v3.48.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e7247d05 | ||
|
|
502f24a2ad | ||
|
|
f09f31c6d5 | ||
|
|
5a78808caa |
@@ -1,7 +1,9 @@
|
|||||||
# Changelog
|
# 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
|
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
|
||||||
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
|
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
|
||||||
@trulede).
|
@trulede).
|
||||||
|
|||||||
@@ -1160,6 +1160,10 @@ func TestIf(t *testing.T) {
|
|||||||
|
|
||||||
// For loop with if
|
// For loop with if
|
||||||
{name: "if-in-for-loop", task: "if-in-for-loop", verbose: true},
|
{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 {
|
for _, test := range tests {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.47.0
|
3.48.0
|
||||||
|
|||||||
37
task.go
37
task.go
@@ -148,6 +148,20 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
|||||||
return nil
|
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 strings.TrimSpace(t.If) != "" {
|
||||||
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||||
Command: t.If,
|
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)
|
prompted, err := e.promptTaskVars(t, call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -176,11 +190,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err = e.CompiledTask(call)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
|
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
|
||||||
return err
|
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 {
|
switch {
|
||||||
case cmd.Task != "":
|
case cmd.Task != "":
|
||||||
reacquire := e.releaseConcurrencyLimit()
|
reacquire := e.releaseConcurrencyLimit()
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Cmd struct {
|
|||||||
IgnoreError bool
|
IgnoreError bool
|
||||||
Defer bool
|
Defer bool
|
||||||
Platforms []*Platform
|
Platforms []*Platform
|
||||||
Ask string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cmd) DeepCopy() *Cmd {
|
func (c *Cmd) DeepCopy() *Cmd {
|
||||||
@@ -39,7 +38,6 @@ func (c *Cmd) DeepCopy() *Cmd {
|
|||||||
IgnoreError: c.IgnoreError,
|
IgnoreError: c.IgnoreError,
|
||||||
Defer: c.Defer,
|
Defer: c.Defer,
|
||||||
Platforms: deepcopy.Slice(c.Platforms),
|
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"`
|
IgnoreError bool `yaml:"ignore_error"`
|
||||||
Defer *Defer
|
Defer *Defer
|
||||||
Platforms []*Platform
|
Platforms []*Platform
|
||||||
Ask string
|
|
||||||
}
|
}
|
||||||
if err := node.Decode(&cmdStruct); err != nil {
|
if err := node.Decode(&cmdStruct); err != nil {
|
||||||
return errors.NewTaskfileDecodeError(err, node)
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
@@ -101,7 +98,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
c.If = cmdStruct.If
|
c.If = cmdStruct.If
|
||||||
c.Silent = cmdStruct.Silent
|
c.Silent = cmdStruct.Silent
|
||||||
c.IgnoreError = cmdStruct.IgnoreError
|
c.IgnoreError = cmdStruct.IgnoreError
|
||||||
c.Ask = cmdStruct.Ask
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +111,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
c.Shopt = cmdStruct.Shopt
|
c.Shopt = cmdStruct.Shopt
|
||||||
c.IgnoreError = cmdStruct.IgnoreError
|
c.IgnoreError = cmdStruct.IgnoreError
|
||||||
c.Platforms = cmdStruct.Platforms
|
c.Platforms = cmdStruct.Platforms
|
||||||
c.Ask = cmdStruct.Ask
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
testdata/if/Taskfile.yml
vendored
18
testdata/if/Taskfile.yml
vendored
@@ -158,3 +158,21 @@ tasks:
|
|||||||
if: '{{ eq .ENV "dev" }}'
|
if: '{{ eq .ENV "dev" }}'
|
||||||
cmds:
|
cmds:
|
||||||
- echo "should not appear"
|
- 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"
|
||||||
|
|||||||
2
testdata/if/testdata/TestIf-task-if-dynamic-false.golden
vendored
Normal file
2
testdata/if/testdata/TestIf-task-if-dynamic-false.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
task: dynamic variable: "echo \"false\"" result: "false"
|
||||||
|
task: if condition not met - skipped: "task-if-dynamic-false"
|
||||||
1
testdata/if/testdata/TestIf-task-if-dynamic-true.golden
vendored
Normal file
1
testdata/if/testdata/TestIf-task-if-dynamic-true.golden
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dynamic feature enabled
|
||||||
@@ -7,6 +7,20 @@ outline: deep
|
|||||||
|
|
||||||
::: v-pre
|
::: 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
|
## v3.47.0 - 2026-01-24
|
||||||
|
|
||||||
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
|
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
|
||||||
|
|||||||
@@ -741,8 +741,6 @@ tasks:
|
|||||||
platforms: [linux, darwin]
|
platforms: [linux, darwin]
|
||||||
set: [errexit]
|
set: [errexit]
|
||||||
shopt: [globstar]
|
shopt: [globstar]
|
||||||
if: '[ "$CI" = "true" ]'
|
|
||||||
ask: "Run this command?"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task References
|
### Task References
|
||||||
@@ -859,36 +857,6 @@ tasks:
|
|||||||
if: '[ "{{.ITEM}}" != "b" ]'
|
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
|
## Shell Options
|
||||||
|
|
||||||
### Set Options
|
### Set Options
|
||||||
|
|||||||
@@ -340,10 +340,6 @@
|
|||||||
"if": {
|
"if": {
|
||||||
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
||||||
"type": "string"
|
"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,
|
"additionalProperties": false,
|
||||||
@@ -385,10 +381,6 @@
|
|||||||
"if": {
|
"if": {
|
||||||
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
|
||||||
"ask": {
|
|
||||||
"description": "A y/n confirmation shown before executing this command. If declined, the command is skipped.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user