feat: add command-level timeout support

Add a per-command `timeout` option that terminates a command once it
exceeds the given duration, preventing commands from hanging indefinitely
in a pipeline. Uses Go duration syntax (e.g. 30s, 5m, 1h30m) and applies
to both shell commands and task calls.

Closes #1569
This commit is contained in:
Valentin Maerten
2026-06-30 10:29:20 +02:00
parent a61f8ade36
commit 4d5f7337c1
6 changed files with 147 additions and 0 deletions

12
task.go
View File

@@ -376,12 +376,21 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
}
}
if cmd.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, cmd.Timeout)
defer cancel()
}
switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil && ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("task: [%s] command timeout exceeded (%s): %w", t.Name(), cmd.Timeout, err)
}
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
@@ -426,6 +435,9 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
}
if err != nil && ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("task: [%s] command timeout exceeded (%s): %w", t.Name(), cmd.Timeout, err)
}
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)