1
0
mirror of https://github.com/go-task/task.git synced 2025-11-23 22:24:45 +02:00

feat: add --failfast and failtest: true to control dependencies

This commit is contained in:
Andrey Nering
2025-11-22 18:08:18 -03:00
parent 7901cce831
commit 734fdba570
21 changed files with 191 additions and 2 deletions

View File

@@ -2,6 +2,11 @@
## Unreleased
- A small behavior change was made to dependencies. Task will now wait for all
dependencies to finish running before continuing, even if any of them fail.
To opt for the previous behavior, set `failfast: true` either globally or per
task, or use the `--failfast` flag, which will also work for `--parallel`
(#1246, #2525 by @andreynering).
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times

View File

@@ -47,6 +47,7 @@ type (
Color bool
Concurrency int
Interval time.Duration
Failfast bool
// I/O
Stdin io.Reader
@@ -502,3 +503,16 @@ type versionCheckOption struct {
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
e.EnableVersionCheck = o.enableVersionCheck
}
// WithFailfast tells the [Executor] whether or not to check the version of
func WithFailfast(failfast bool) ExecutorOption {
return &failfastOption{failfast}
}
type failfastOption struct {
failfast bool
}
func (o *failfastOption) ApplyToExecutor(e *Executor) {
e.Failfast = o.failfast
}

View File

@@ -996,3 +996,64 @@ func TestIncludeChecksum(t *testing.T) {
WithFixtureTemplating(),
)
}
func TestFailfast(t *testing.T) {
t.Parallel()
t.Run("Default", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Option", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
task.WithFailfast(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Global", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("global"),
WithExecutorOptions(
task.WithDir("testdata/failfast/global"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Task", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("task"),
WithExecutorOptions(
task.WithDir("testdata/failfast/task"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
}

View File

@@ -69,6 +69,7 @@ var (
Output ast.Output
Color bool
Interval time.Duration
Failfast bool
Global bool
Experiments bool
Download bool
@@ -137,6 +138,7 @@ func init() {
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVar(&Failfast, "failfast", false, "When running tasks in parallel, stop all tasks if one fails.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -253,6 +255,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithOutputStyle(Output),
task.WithTaskSorter(sorter),
task.WithVersionCheck(true),
task.WithFailfast(Failfast),
)
}

10
task.go
View File

@@ -78,7 +78,10 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return err
}
g, ctx := errgroup.WithContext(ctx)
g := &errgroup.Group{}
if e.Failfast || e.Taskfile.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
for _, c := range regularCalls {
if e.Parallel {
g.Go(func() error { return e.RunTask(ctx, c) })
@@ -257,7 +260,10 @@ func (e *Executor) mkdir(t *ast.Task) error {
}
func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
g, ctx := errgroup.WithContext(ctx)
g := &errgroup.Group{}
if e.Failfast || e.Taskfile.Failfast || t.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
reacquire := e.releaseConcurrencyLimit()
defer reacquire()

View File

@@ -42,6 +42,7 @@ type Task struct {
Platforms []*Platform
Watch bool
Location *Location
Failfast bool
// Populated during merging
Namespace string `hash:"ignore"`
IncludeVars *Vars
@@ -143,6 +144,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Platforms []*Platform
Requires *Requires
Watch bool
Failfast bool
}
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -181,6 +183,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Platforms = task.Platforms
t.Requires = task.Requires
t.Watch = task.Watch
t.Failfast = task.Failfast
return nil
}
@@ -226,6 +229,7 @@ func (t *Task) DeepCopy() *Task {
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FullName: t.FullName,
Failfast: t.Failfast,
}
return c
}

View File

@@ -34,6 +34,7 @@ type Taskfile struct {
Dotenv []string
Run string
Interval time.Duration
Failfast bool
}
// Merge merges the second Taskfile into the first
@@ -81,6 +82,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Dotenv []string
Run string
Interval time.Duration
Failfast bool
}
if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -98,6 +100,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
tf.Failfast = taskfile.Failfast
if tf.Includes == nil {
tf.Includes = NewIncludes()
}

14
testdata/failfast/default/Taskfile.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1,3 @@
dep1
dep2
dep3

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1 @@

16
testdata/failfast/global/Taskfile.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: '3'
failfast: true
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1 @@

15
testdata/failfast/task/Taskfile.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
failfast: true
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1 @@

View File

@@ -71,6 +71,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
Requires: origTask.Requires,
Watch: origTask.Watch,
Namespace: origTask.Namespace,
Failfast: origTask.Failfast,
}, nil
}
@@ -125,6 +126,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
Location: origTask.Location,
Requires: origTask.Requires,
Watch: origTask.Watch,
Failfast: origTask.Failfast,
Namespace: origTask.Namespace,
FullName: fullName,
}

View File

@@ -591,6 +591,32 @@ tasks:
- echo {{.TEXT}}
```
### Fail-fast dependencies
By default, Task waits for all dependencies to finish running before continuing.
If you want Task to stop executing further dependencies as soon as one fails,
you can set `failfast: true`:
```yaml
version: '3'
failfast: true # global option so it applies to all tasks
tasks:
# ...
```
```yaml
version: '3'
tasks:
default:
deps: [task1, task2, task3]
failfast: true # applies only to this task
```
Alternatively, you can use `--failfast`, which also work for `--parallel`.
## Platform specific tasks and commands
If you want to restrict the running of tasks to explicit platforms, this can be

View File

@@ -201,6 +201,11 @@
"description": "Configures a task to run in watch mode automatically.",
"type": "boolean",
"default": false
},
"failfast": {
"description": "When running tasks in parallel, stop all tasks if one fails.",
"type": "boolean",
"default": false
}
}
},
@@ -740,6 +745,11 @@
"description": "Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
"type": "string",
"pattern": "^[0-9]+(?:m|s|ms)$"
},
"failfast": {
"description": "When running tasks in parallel, stop all tasks if one fails.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false,