Skip to content

Commit

Permalink
run: add --fail-fast parameter
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fho committed Nov 12, 2024
1 parent 0c655e2 commit 7b8abe1
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 9 deletions.
16 changes: 11 additions & 5 deletions internal/command/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type runCmd struct {
// Cmdline parameters
skipUpload bool
force bool
failFast bool
inputStr []string
lookupInputStr string
taskRunnerGoRoutines uint
Expand Down Expand Up @@ -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", "",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
})
}

Expand Down
57 changes: 56 additions & 1 deletion internal/command/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -404,6 +404,7 @@ func TestRunAbortsAfterError(t *testing.T) {
doInitDb(t)

runCmdTest := newRunCmd()
runCmdTest.SetArgs([]string{"--fail-fast"})
_, stderr := interceptCmdOutput(t)

oldExitFunc := exitFunc
Expand All @@ -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")
}
6 changes: 4 additions & 2 deletions pkg/baur/taskrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/baur/taskrunner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 7b8abe1

Please sign in to comment.