feat(cmd): add ask option to commands

Add an `ask` attribute to commands that shows a y/n confirmation
before executing. If the user declines, the command is skipped but
the task continues with the next command.

Example:
```yaml
cmds:
  - cmd: echo "Deploying..."
    ask: "Deploy to production?"
  - task: run-migrations
    ask: "Run database migrations?"
```

Behavior:
- Default: asks for y/n confirmation
- --yes: auto-confirms all asks
- --dry: shows commands without asking

This differs from task-level `prompt:` which cancels the entire task
if declined. Command-level `ask:` only skips the individual command.
This commit is contained in:
Valentin Maerten
2026-01-25 22:14:19 +01:00
parent 026c899d90
commit 93cdccefce
4 changed files with 61 additions and 0 deletions

16
task.go
View File

@@ -365,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 {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()

View File

@@ -20,6 +20,7 @@ type Cmd struct {
IgnoreError bool
Defer bool
Platforms []*Platform
Ask string
}
func (c *Cmd) DeepCopy() *Cmd {
@@ -38,6 +39,7 @@ func (c *Cmd) DeepCopy() *Cmd {
IgnoreError: c.IgnoreError,
Defer: c.Defer,
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"`
Defer *Defer
Platforms []*Platform
Ask string
}
if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -98,6 +101,7 @@ 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
}
@@ -111,6 +115,7 @@ 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

@@ -741,6 +741,8 @@ tasks:
platforms: [linux, darwin]
set: [errexit]
shopt: [globstar]
if: '[ "$CI" = "true" ]'
ask: "Run this command?"
```
### Task References
@@ -857,6 +859,36 @@ 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,6 +340,10 @@
"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,
@@ -381,6 +385,10 @@
"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,