mirror of
https://github.com/go-task/task.git
synced 2026-06-30 16:14:19 +00:00
Compare commits
1 Commits
nightly
...
feat/comma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5f7337c1 |
12
task.go
12
task.go
@@ -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)
|
||||
|
||||
57
task_test.go
57
task_test.go
@@ -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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
@@ -21,6 +23,7 @@ type Cmd struct {
|
||||
IgnoreError bool
|
||||
Defer bool
|
||||
Platforms []*Platform
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Cmd) DeepCopy() *Cmd {
|
||||
@@ -40,6 +43,7 @@ func (c *Cmd) DeepCopy() *Cmd {
|
||||
IgnoreError: c.IgnoreError,
|
||||
Defer: c.Defer,
|
||||
Platforms: deepcopy.Slice(c.Platforms),
|
||||
Timeout: c.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +71,20 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Defer *Defer
|
||||
Platforms []*Platform
|
||||
Timeout string
|
||||
}
|
||||
if err := node.Decode(&cmdStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
if cmdStruct.Timeout != "" {
|
||||
timeout, err := time.ParseDuration(cmdStruct.Timeout)
|
||||
if err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node).WithMessage("invalid timeout format")
|
||||
}
|
||||
c.Timeout = timeout
|
||||
}
|
||||
|
||||
if cmdStruct.Defer != nil {
|
||||
|
||||
// A deferred command
|
||||
|
||||
29
testdata/timeout/Taskfile.yml
vendored
Normal file
29
testdata/timeout/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
timeout-exceeded:
|
||||
desc: Command that should timeout
|
||||
cmds:
|
||||
- cmd: sleep 10
|
||||
timeout: 1s
|
||||
|
||||
timeout-not-exceeded:
|
||||
desc: Command that completes within timeout
|
||||
cmds:
|
||||
- cmd: echo "quick command"
|
||||
timeout: 5s
|
||||
|
||||
no-timeout:
|
||||
desc: Command with no timeout specified
|
||||
cmds:
|
||||
- echo "no timeout"
|
||||
|
||||
multiple-cmds-timeout:
|
||||
desc: Multiple commands where one exceeds its timeout
|
||||
cmds:
|
||||
- cmd: echo "first"
|
||||
timeout: 1s
|
||||
- cmd: sleep 10
|
||||
timeout: 1s
|
||||
- cmd: echo "third"
|
||||
timeout: 1s
|
||||
@@ -798,6 +798,7 @@ tasks:
|
||||
platforms: [linux, darwin]
|
||||
set: [errexit]
|
||||
shopt: [globstar]
|
||||
timeout: 5m
|
||||
```
|
||||
|
||||
### Task References
|
||||
@@ -914,6 +915,24 @@ tasks:
|
||||
if: '[ "{{.ITEM}}" != "b" ]'
|
||||
```
|
||||
|
||||
### Command Timeouts
|
||||
|
||||
Use `timeout` to limit how long a command may run. The value uses Go duration
|
||||
syntax (e.g. `30s`, `5m`, `1h30m`).
|
||||
|
||||
```yaml
|
||||
tasks:
|
||||
deploy:
|
||||
cmds:
|
||||
- cmd: npm run build
|
||||
timeout: 5m
|
||||
- cmd: ./deploy.sh
|
||||
timeout: 30m
|
||||
```
|
||||
|
||||
When a command exceeds its timeout, it is terminated and the task fails with an
|
||||
error, preventing commands from hanging indefinitely in a pipeline.
|
||||
|
||||
## Shell Options
|
||||
|
||||
### Set Options
|
||||
|
||||
@@ -352,6 +352,10 @@
|
||||
"if": {
|
||||
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -393,6 +397,10 @@
|
||||
"if": {
|
||||
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -445,6 +453,10 @@
|
||||
"platforms": {
|
||||
"description": "Specifies which platforms the command should be run on.",
|
||||
"$ref": "#/definitions/platforms"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -475,6 +487,10 @@
|
||||
"if": {
|
||||
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Reference in New Issue
Block a user