Skip to content

feat: add failfast support for parallel tasks #2307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ func run() error {
return err
}

tf := e.Taskfile
if flags.FailFast && tf != nil && tf.Tasks != nil {
for t := range tf.Tasks.Values(nil) {
t.FailFast = true
}
}

if flags.ClearCache {
cachePath := filepath.Join(e.TempDir.Remote, "remote")
return os.RemoveAll(cachePath)
Expand Down
3 changes: 3 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var (
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
FailFast bool
)

func init() {
Expand Down Expand Up @@ -156,6 +157,8 @@ func init() {
pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.")
}

pflag.BoolVar(&FailFast, "failfast", false, "Run parallel deps to completion but still exit non-zero if any failed.")

pflag.Parse()
}

Expand Down
51 changes: 40 additions & 11 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"runtime"
"slices"
"sync"
"sync/atomic"

"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -259,23 +260,51 @@ func (e *Executor) mkdir(t *ast.Task) error {
}

func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
g, ctx := errgroup.WithContext(ctx)

reacquire := e.releaseConcurrencyLimit()
defer reacquire()

for _, d := range t.Deps {
d := d
g.Go(func() error {
if t.FailFast {
g, ctx := errgroup.WithContext(ctx)
for _, d := range t.Deps {
d := d
g.Go(func() error {
return e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
})
}
return g.Wait()
}

type depResult struct {
idx int
err error
}

results := make(chan depResult, len(t.Deps))
var wg sync.WaitGroup
wg.Add(len(t.Deps))

for i, d := range t.Deps {
i, d := i, d
go func() {
defer wg.Done()
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil {
return err
}
return nil
})
results <- depResult{idx: i, err: err}
}()
}

return g.Wait()
wg.Wait()
close(results)

var firstErr error
for res := range results {
if res.err != nil && firstErr == nil {
firstErr = res.err
}
}
if firstErr != nil {
return fmt.Errorf("one or more dependencies failed: %w", firstErr)
}
return nil
}

func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
Expand Down
4 changes: 4 additions & 0 deletions taskfile/ast/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Task struct {
Namespace string
IncludeVars *Vars
IncludedTaskfileVars *Vars
FailFast bool
}

func (t *Task) Name() string {
Expand Down Expand Up @@ -138,6 +139,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Platforms []*Platform
Requires *Requires
Watch bool
FailFast bool `yaml:"failfast"`
}
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
Expand Down Expand Up @@ -176,6 +178,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
}

Expand Down Expand Up @@ -220,6 +223,7 @@ func (t *Task) DeepCopy() *Task {
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FailFast: t.FailFast,
}
return c
}
1 change: 1 addition & 0 deletions variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
Requires: origTask.Requires,
Watch: origTask.Watch,
Namespace: origTask.Namespace,
FailFast: origTask.FailFast,
}
new.Dir, err = execext.ExpandLiteral(new.Dir)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion website/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ If `--` is given, all remaining arguments will be assigned to a special
| `-C` | `--concurrency` | `int` | `0` | Limit number tasks to run concurrently. Zero means unlimited. |
| `-d` | `--dir` | `string` | Working directory | Sets the directory in which Task will execute and look for a Taskfile. |
| `-n` | `--dry` | `bool` | `false` | Compiles and prints tasks in the order that they would be run, without executing them. |
| `-x` | `--exit-code` | `bool` | `false` | Pass-through the exit code of the task command. |
| `-x` | `--exit-code` | `bool` | `false` | Pass-through the exit code of the task command.
| | `--failfast` | `bool` | `false` | Run parallel deps to completion but still exit non-zero if any failed. |
| `-f` | `--force` | `bool` | `false` | Forces execution even when the task is up-to-date. |
| `-g` | `--global` | `bool` | `false` | Runs global Taskfile, from `$HOME/Taskfile.{yml,yaml}`. |
| `-h` | `--help` | `bool` | `false` | Shows Task usage. |
Expand Down
3 changes: 2 additions & 1 deletion website/docs/reference/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ vars:
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). Task will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html).
| `failfast` | `bool` | `false` | Run parallel deps to completion but still exit non-zero if any failed. |

:::info

Expand Down