mirror of
https://github.com/go-task/task.git
synced 2026-07-01 16:44:34 +00:00
Compare commits
4 Commits
feat/comma
...
docs/homeb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9d6b73a5 | ||
|
|
48b215db0a | ||
|
|
0aa0496f85 | ||
|
|
d601746d4b |
17
.github/renovate.json
vendored
17
.github/renovate.json
vendored
@@ -6,20 +6,31 @@
|
|||||||
"schedule:weekly",
|
"schedule:weekly",
|
||||||
":semanticCommitTypeAll(chore)"
|
":semanticCommitTypeAll(chore)"
|
||||||
],
|
],
|
||||||
"mode": "full",
|
|
||||||
"addLabels":["area: dependencies"],
|
"addLabels":["area: dependencies"],
|
||||||
|
"osvVulnerabilityAlerts": true,
|
||||||
|
"postUpdateOptions": ["gomodTidy"],
|
||||||
"customManagers": [
|
"customManagers": [
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"],
|
"managerFilePatterns": ["/^\\.github/workflows/.*\\.ya?ml$/"],
|
||||||
"matchStrings": [
|
"matchStrings": [
|
||||||
"uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
|
"uses:\\s*golangci/golangci-lint-action@\\S+(?:\\s*#[^\\n]*)?\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
|
||||||
],
|
],
|
||||||
"datasourceTemplate": "github-releases",
|
"datasourceTemplate": "github-releases",
|
||||||
"depNameTemplate": "golangci/golangci-lint"
|
"depNameTemplate": "golangci/golangci-lint"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["gomod"],
|
||||||
|
"matchDepTypes": ["indirect"],
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["digest", "pinDigest"],
|
||||||
|
"groupName": "all non-major dependencies",
|
||||||
|
"groupSlug": "all-minor-patch"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["github-actions"],
|
"matchManagers": ["github-actions"],
|
||||||
"addLabels": ["area: github actions"]
|
"addLabels": ["area: github actions"]
|
||||||
|
|||||||
@@ -80,22 +80,21 @@ nfpms:
|
|||||||
- src: completion/zsh/_task
|
- src: completion/zsh/_task
|
||||||
dst: /usr/local/share/zsh/site-functions/_task
|
dst: /usr/local/share/zsh/site-functions/_task
|
||||||
|
|
||||||
brews:
|
homebrew_casks:
|
||||||
- name: go-task
|
- name: go-task
|
||||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||||
license: MIT
|
|
||||||
homepage: https://taskfile.dev
|
homepage: https://taskfile.dev
|
||||||
directory: Formula
|
binaries:
|
||||||
|
- task
|
||||||
|
completions:
|
||||||
|
bash: completion/bash/task.bash
|
||||||
|
zsh: completion/zsh/_task
|
||||||
|
fish: completion/fish/task.fish
|
||||||
|
directory: Casks
|
||||||
repository:
|
repository:
|
||||||
owner: go-task
|
owner: go-task
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
|
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
|
||||||
test: system "#{bin}/task", "--help"
|
|
||||||
install: |-
|
|
||||||
bin.install "task"
|
|
||||||
bash_completion.install "completion/bash/task.bash" => "task"
|
|
||||||
zsh_completion.install "completion/zsh/_task" => "_task"
|
|
||||||
fish_completion.install "completion/fish/task.fish"
|
|
||||||
commit_author:
|
commit_author:
|
||||||
name: task-bot
|
name: task-bot
|
||||||
email: 106601941+task-bot@users.noreply.github.com
|
email: 106601941+task-bot@users.noreply.github.com
|
||||||
|
|||||||
12
task.go
12
task.go
@@ -376,21 +376,12 @@ 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 {
|
switch {
|
||||||
case cmd.Task != "":
|
case cmd.Task != "":
|
||||||
reacquire := e.releaseConcurrencyLimit()
|
reacquire := e.releaseConcurrencyLimit()
|
||||||
defer reacquire()
|
defer reacquire()
|
||||||
|
|
||||||
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
|
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
|
var exitCode interp.ExitStatus
|
||||||
if errors.As(err, &exitCode) && cmd.IgnoreError {
|
if errors.As(err, &exitCode) && cmd.IgnoreError {
|
||||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
|
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
|
||||||
@@ -435,9 +426,6 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
|
|||||||
if closeErr := closer(err); closeErr != nil {
|
if closeErr := closer(err); closeErr != nil {
|
||||||
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
|
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
|
var exitCode interp.ExitStatus
|
||||||
if errors.As(err, &exitCode) && cmd.IgnoreError {
|
if errors.As(err, &exitCode) && cmd.IgnoreError {
|
||||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
|
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
|
||||||
|
|||||||
57
task_test.go
57
task_test.go
@@ -2497,63 +2497,6 @@ 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
|
func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel
|
||||||
const dir = "testdata/evaluate_symlinks_in_paths"
|
const dir = "testdata/evaluate_symlinks_in_paths"
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package ast
|
package ast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.yaml.in/yaml/v3"
|
"go.yaml.in/yaml/v3"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
@@ -23,7 +21,6 @@ type Cmd struct {
|
|||||||
IgnoreError bool
|
IgnoreError bool
|
||||||
Defer bool
|
Defer bool
|
||||||
Platforms []*Platform
|
Platforms []*Platform
|
||||||
Timeout time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cmd) DeepCopy() *Cmd {
|
func (c *Cmd) DeepCopy() *Cmd {
|
||||||
@@ -43,7 +40,6 @@ 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),
|
||||||
Timeout: c.Timeout,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,20 +67,10 @@ 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
|
||||||
Timeout 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if cmdStruct.Defer != nil {
|
||||||
|
|
||||||
// A deferred command
|
// A deferred command
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ func (vars *Vars) Values() iter.Seq[Var] {
|
|||||||
// ToCacheMap converts Vars to an unordered map containing only the static
|
// ToCacheMap converts Vars to an unordered map containing only the static
|
||||||
// variables
|
// variables
|
||||||
func (vars *Vars) ToCacheMap() (m map[string]any) {
|
func (vars *Vars) ToCacheMap() (m map[string]any) {
|
||||||
|
if vars == nil || vars.om == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
defer vars.mutex.RUnlock()
|
defer vars.mutex.RUnlock()
|
||||||
vars.mutex.RLock()
|
vars.mutex.RLock()
|
||||||
m = make(map[string]any, vars.Len())
|
m = make(map[string]any, vars.Len())
|
||||||
|
|||||||
55
taskfile/ast/vars_test.go
Normal file
55
taskfile/ast/vars_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVars_ToCacheMap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("nil receiver returns nil", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var vars *Vars
|
||||||
|
assert.Nil(t, vars.ToCacheMap())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty vars returns empty map", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
vars := NewVars()
|
||||||
|
m := vars.ToCacheMap()
|
||||||
|
assert.NotNil(t, m)
|
||||||
|
assert.Empty(t, m)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("static values are included", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
vars := NewVars(
|
||||||
|
&VarElement{Key: "FOO", Value: Var{Value: "bar"}},
|
||||||
|
&VarElement{Key: "NUM", Value: Var{Value: 42}},
|
||||||
|
)
|
||||||
|
m := vars.ToCacheMap()
|
||||||
|
assert.Equal(t, map[string]any{"FOO": "bar", "NUM": 42}, m)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("live values take precedence over static values", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
vars := NewVars(
|
||||||
|
&VarElement{Key: "FOO", Value: Var{Value: "bar", Live: "live-bar"}},
|
||||||
|
)
|
||||||
|
m := vars.ToCacheMap()
|
||||||
|
assert.Equal(t, map[string]any{"FOO": "live-bar"}, m)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dynamic variables are excluded", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
sh := "echo hello"
|
||||||
|
vars := NewVars(
|
||||||
|
&VarElement{Key: "STATIC", Value: Var{Value: "ok"}},
|
||||||
|
&VarElement{Key: "DYNAMIC", Value: Var{Sh: &sh}},
|
||||||
|
)
|
||||||
|
m := vars.ToCacheMap()
|
||||||
|
assert.Equal(t, map[string]any{"STATIC": "ok"}, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
29
testdata/timeout/Taskfile.yml
vendored
29
testdata/timeout/Taskfile.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -75,7 +75,7 @@ apk add task
|
|||||||
### [Homebrew](https://brew.sh)   {#homebrew}
|
### [Homebrew](https://brew.sh)   {#homebrew}
|
||||||
|
|
||||||
Task is available via our official Homebrew tap
|
Task is available via our official Homebrew tap
|
||||||
[[source](https://github.com/go-task/homebrew-tap/blob/main/Formula/go-task.rb)]:
|
[[source](https://github.com/go-task/homebrew-tap/blob/main/Casks/go-task.rb)]:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew install go-task/tap/go-task
|
brew install go-task/tap/go-task
|
||||||
|
|||||||
@@ -798,7 +798,6 @@ tasks:
|
|||||||
platforms: [linux, darwin]
|
platforms: [linux, darwin]
|
||||||
set: [errexit]
|
set: [errexit]
|
||||||
shopt: [globstar]
|
shopt: [globstar]
|
||||||
timeout: 5m
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task References
|
### Task References
|
||||||
@@ -915,24 +914,6 @@ tasks:
|
|||||||
if: '[ "{{.ITEM}}" != "b" ]'
|
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
|
## Shell Options
|
||||||
|
|
||||||
### Set Options
|
### Set Options
|
||||||
|
|||||||
@@ -352,10 +352,6 @@
|
|||||||
"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"
|
||||||
},
|
|
||||||
"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,
|
"additionalProperties": false,
|
||||||
@@ -397,10 +393,6 @@
|
|||||||
"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"
|
||||||
},
|
|
||||||
"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,
|
"additionalProperties": false,
|
||||||
@@ -453,10 +445,6 @@
|
|||||||
"platforms": {
|
"platforms": {
|
||||||
"description": "Specifies which platforms the command should be run on.",
|
"description": "Specifies which platforms the command should be run on.",
|
||||||
"$ref": "#/definitions/platforms"
|
"$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,
|
"additionalProperties": false,
|
||||||
@@ -487,10 +475,6 @@
|
|||||||
"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"
|
||||||
},
|
|
||||||
"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,
|
"additionalProperties": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user