mirror of
https://github.com/go-task/task.git
synced 2026-06-24 21:26:04 +00:00
Compare commits
2 Commits
v3.48.0
...
feat/comma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a96a47c99 | ||
|
|
93cdccefce |
@@ -1,9 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v3.48.0 - 2026-01-26
|
## Unreleased
|
||||||
|
|
||||||
- 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,10 +1160,6 @@ 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.48.0
|
3.47.0
|
||||||
|
|||||||
37
task.go
37
task.go
@@ -148,20 +148,6 @@ 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,
|
||||||
@@ -173,7 +159,7 @@ 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)
|
// Prompt for missing required vars (just-in-time for sequential task calls)
|
||||||
prompted, err := e.promptTaskVars(t, call)
|
prompted, err := e.promptTaskVars(t, call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -190,6 +176,11 @@ 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
|
||||||
}
|
}
|
||||||
@@ -374,6 +365,22 @@ 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,6 +20,7 @@ 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 {
|
||||||
@@ -38,6 +39,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ 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)
|
||||||
@@ -98,6 +101,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +115,7 @@ 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,21 +158,3 @@ 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"
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
task: dynamic variable: "echo \"false\"" result: "false"
|
|
||||||
task: if condition not met - skipped: "task-if-dynamic-false"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
dynamic feature enabled
|
|
||||||
@@ -7,20 +7,6 @@ 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,6 +741,8 @@ 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
|
||||||
@@ -857,6 +859,36 @@ 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,6 +340,10 @@
|
|||||||
"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,
|
||||||
@@ -381,6 +385,10 @@
|
|||||||
"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