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

View File

@@ -2497,6 +2497,63 @@ func TestErrorCode(t *testing.T) {
}
}
func TestCommandTimeout(t *testing.T) {
t.Parallel()
const dir = "testdata/timeout"
tests := []struct {
name string
task string
expectError bool
errorContains string
}{
{
name: "timeout exceeded",
task: "timeout-exceeded",
expectError: true,
errorContains: "timeout exceeded",
},
{
name: "timeout not exceeded",
task: "timeout-not-exceeded",
expectError: false,
},
{
name: "no timeout",
task: "no-timeout",
expectError: false,
},
{
name: "multiple commands with timeout",
task: "multiple-cmds-timeout",
expectError: true,
errorContains: "timeout exceeded",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
err := e.Run(t.Context(), &task.Call{Task: test.task})
if test.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), test.errorContains)
} else {
require.NoError(t, err)
}
})
}
}
func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel
const dir = "testdata/evaluate_symlinks_in_paths"
var buff bytes.Buffer