diff --git a/internal/command/run.go b/internal/command/run.go index 83298485..27b2d5bd 100644 --- a/internal/command/run.go +++ b/internal/command/run.go @@ -84,6 +84,7 @@ type runCmd struct { // Cmdline parameters skipUpload bool force bool + failFast bool inputStr []string lookupInputStr string taskRunnerGoRoutines uint @@ -127,6 +128,8 @@ func newRunCmd() *runCmd { "skip uploading task outputs and recording the run") cmd.Flags().BoolVarP(&cmd.force, "force", "f", false, "enforce running tasks independent of their status") + cmd.Flags().BoolVar(&cmd.failFast, "fail-fast", false, + "skip execution of queued tasks and terminate when a task run fails") cmd.Flags().StringArrayVar(&cmd.inputStr, "input-str", nil, "include a string as input, can be specified multiple times") cmd.Flags().StringVar(&cmd.lookupInputStr, "lookup-input-str", "", @@ -183,6 +186,7 @@ func (c *runCmd) run(_ *cobra.Command, args []string) { c.taskRunnerRoutinePool = routines.NewPool(c.taskRunnerGoRoutines) c.taskRunner = baur.NewTaskRunner( baur.NewTaskInfoCreator(c.storage, taskStatusEvaluator), + c.failFast, ) if c.showOutput && !verboseFlag { @@ -297,13 +301,15 @@ func (c *runCmd) run(_ *cobra.Command, args []string) { func (c *runCmd) skipAllScheduledTaskRuns() { c.skipAllScheduledTaskRunsOnce.Do(func() { c.taskRunner.SkipRuns(true) - c.errorHappened = true + if c.failFast { + stderr.Printf("%s, %s execution of queued task runs\n", + term.RedHighlight("terminating"), + term.YellowHighlight("skipping"), + ) - stderr.Printf("%s, %s execution of queued task runs\n", - term.RedHighlight("terminating"), - term.YellowHighlight("skipping"), - ) + return + } }) } diff --git a/internal/command/run_test.go b/internal/command/run_test.go index b662ddc8..6eea134e 100644 --- a/internal/command/run_test.go +++ b/internal/command/run_test.go @@ -370,7 +370,7 @@ func TestRunFailsWhenGitWorktreeIsDirty(t *testing.T) { require.Contains(t, stderrBuf.String(), "expecting only tracked unmodified files") } -func TestRunAbortsAfterError(t *testing.T) { +func TestRunFalFastAbortsAfterError(t *testing.T) { initTest(t) r := repotest.CreateBaurRepository(t, repotest.WithNewDB()) @@ -404,6 +404,7 @@ func TestRunAbortsAfterError(t *testing.T) { doInitDb(t) runCmdTest := newRunCmd() + runCmdTest.SetArgs([]string{"--fail-fast"}) _, stderr := interceptCmdOutput(t) oldExitFunc := exitFunc @@ -422,3 +423,57 @@ func TestRunAbortsAfterError(t *testing.T) { assert.Contains(t, stderr.String(), "terminating, skipping execution of queued task runs") assert.Contains(t, stderr.String(), "testapp.xbuild: execution skipped") } + +func TestRunFailFastDisabledContinuesAfterError(t *testing.T) { + initTest(t) + r := repotest.CreateBaurRepository(t, repotest.WithNewDB()) + + appCfg := cfg.App{ + Name: "testapp", + Tasks: cfg.Tasks{ + { + Name: "build", + Command: []string{"bash", "-c", "exit 1"}, + Input: cfg.Input{ + Files: []cfg.FileInputs{ + {Paths: []string{".app.toml"}}, + }, + }, + }, + { + Name: "xbuild", + Command: []string{"bash", "-c", "exit 0"}, + Input: cfg.Input{ + Files: []cfg.FileInputs{ + {Paths: []string{".app.toml"}}, + }, + }, + }, + }, + } + + err := appCfg.ToFile(filepath.Join(r.Dir, ".app.toml")) + require.NoError(t, err) + + doInitDb(t) + + runCmdTest := newRunCmd() + runCmdTest.SetArgs([]string{"--fail-fast=false"}) + stdout, stderr := interceptCmdOutput(t) + + oldExitFunc := exitFunc + var exitCode int + exitFunc = func(code int) { + exitCode = code + } + t.Cleanup(func() { + exitFunc = oldExitFunc + }) + + err = runCmdTest.Execute() + require.NoError(t, err) + assert.Equal(t, 1, exitCode) + + assert.Regexp(t, "^testapp.build.*failed: exit status 1", stderr.String()) + assert.Contains(t, stdout.String(), "testapp.xbuild: run stored in database") +} diff --git a/pkg/baur/taskrunner.go b/pkg/baur/taskrunner.go index ffb70cb2..bfd48445 100644 --- a/pkg/baur/taskrunner.go +++ b/pkg/baur/taskrunner.go @@ -31,16 +31,18 @@ type TaskInfoRetriever interface { // TaskRunner executes the command of a task. type TaskRunner struct { + skipAfterError bool skipEnabled uint32 // must be accessed via atomic operations LogFn exec.PrintfFn GitUntrackedFilesFn func(dir string) ([]string, error) taskInfoCreator *TaskInfoCreator } -func NewTaskRunner(taskInfoCreator *TaskInfoCreator) *TaskRunner { +func NewTaskRunner(taskInfoCreator *TaskInfoCreator, skipAfterError bool) *TaskRunner { return &TaskRunner{ LogFn: exec.DefaultLogFn, taskInfoCreator: taskInfoCreator, + skipAfterError: skipAfterError, } } @@ -90,7 +92,7 @@ func (t *TaskRunner) createTaskInfoEnv(ctx context.Context, task *Task) ([]strin // Run executes the command of a task and returns the execution result. // The output of the commands are logged with debug log level. func (t *TaskRunner) Run(task *Task) (*RunResult, error) { - if t.SkipRunsIsEnabled() { + if t.skipAfterError && t.SkipRunsIsEnabled() { return nil, ErrTaskRunSkipped } if t.GitUntrackedFilesFn != nil { diff --git a/pkg/baur/taskrunner_test.go b/pkg/baur/taskrunner_test.go index 2d3377f0..0e876c23 100644 --- a/pkg/baur/taskrunner_test.go +++ b/pkg/baur/taskrunner_test.go @@ -7,7 +7,7 @@ import ( ) func TestRunningTaskFailsWhenGitWorktreeIsDirty(t *testing.T) { - tr := NewTaskRunner(nil) + tr := NewTaskRunner(nil, true) tr.GitUntrackedFilesFn = func(_ string) ([]string, error) { return []string{"1"}, nil }