From 7b8abe152df828be03bdf2efdfe64585258cfbb2 Mon Sep 17 00:00:00 2001 From: Fabian Holler Date: Tue, 12 Nov 2024 14:48:18 +0100 Subject: [PATCH] run: add --fail-fast parameter Add a "--fail-fast" parameter to "baur run". By default it is disabled, to keep downwards compatibility with the current baur version If enabled, "baur run" continues to execute tasks when one fails. When disabled and a task execution to fails, execution of other queued tasks are skipped. Concurrently running tasks are not aborted. --- internal/command/run.go | 16 ++++++---- internal/command/run_test.go | 57 +++++++++++++++++++++++++++++++++++- pkg/baur/taskrunner.go | 6 ++-- pkg/baur/taskrunner_test.go | 2 +- 4 files changed, 72 insertions(+), 9 deletions(-) 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 }