diff --git a/cmd/task/task.go b/cmd/task/task.go index 0a31dd7a6d..d168676a56 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -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) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 5787e17a6d..c2b4c80a0b 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -76,6 +76,7 @@ var ( ClearCache bool Timeout time.Duration CacheExpiryDuration time.Duration + FailFast bool ) func init() { @@ -156,6 +157,9 @@ func init() { pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 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() } diff --git a/task.go b/task.go index db8b8a9d7c..69f42a2cc1 100644 --- a/task.go +++ b/task.go @@ -6,6 +6,7 @@ import ( "os" "runtime" "slices" + "sync" "sync/atomic" "golang.org/x/sync/errgroup" @@ -258,23 +259,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) { diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 7c49e9a32c..f72a1e09a5 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -48,6 +48,7 @@ type Task struct { IncludedTaskfileVars *Vars FullName string + FailFast bool } func (t *Task) Name() string { @@ -143,6 +144,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) @@ -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 == nil || *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 } diff --git a/variables.go b/variables.go index 2bdd58a256..3ead2a15c5 100644 --- a/variables.go +++ b/variables.go @@ -82,6 +82,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err Watch: origTask.Watch, Namespace: origTask.Namespace, FullName: fullName, + FailFast: origTask.FailFast, } new.Dir, err = execext.ExpandLiteral(new.Dir) if err != nil { diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index 848f492cf1..94956c283e 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -220,6 +220,14 @@ task build --color=false NO_COLOR=1 task build ``` +#### `--failfast` + +Run deps and stop on first failure. Enabled by default. + +```bash +task build --failfast=false +``` + ### Task Information #### `--status` diff --git a/website/src/docs/reference/schema.md b/website/src/docs/reference/schema.md index a6d81c3515..e4b87b3995 100644 --- a/website/src/docs/reference/schema.md +++ b/website/src/docs/reference/schema.md @@ -667,6 +667,26 @@ tasks: - go build -o app ./cmd ``` +#### `failfast` + +- **Type**: `bool` +- **Default**: `true` +- **Description**: Run deps and stop on first failure. + +```yaml +tasks: + task-a: + cmds: [ "bash -c 'echo A; sleep 1; exit 1'" ] + task-b: + cmds: [ "bash -c 'echo B; sleep 2; exit 0'" ] + task-c: + cmds: [ "bash -c 'echo C; sleep 3; exit 1'" ] + + parent: + deps: [task-a, task-b, task-c] + failfast: false +``` + ## Command Individual command configuration within a task.