• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

samsarahq / taskrunner / 25516302028

07 May 2026 07:05PM UTC coverage: 40.25% (+2.0%) from 38.25%
25516302028

push

github

samloop
executor: tag OnStart/OnStop hook errors with hook index

When a hook returns an error, the run aborts with a bare error message
that doesn't say which hook failed. The cache snapshot hook hitting
exit 128 was particularly hard to trace because the surfaced error was
just "exit status 128" with no indication it came from the OnStart
phase. Wrapping with oops.Wrapf("OnStart/OnStop hook %d failed", i)
gives future failures a stack frame and a phase tag.

2 of 4 new or added lines in 1 file covered. (50.0%)

28 existing lines in 2 files now uncovered.

836 of 2077 relevant lines covered (40.25%)

3.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.87
/executor.go
1
package taskrunner
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "html/template"
8
        "os"
9
        "strconv"
10
        "strings"
11
        "sync"
12
        "time"
13

14
        "github.com/samsarahq/go/oops"
15
        "github.com/samsarahq/taskrunner/config"
16
        "github.com/samsarahq/taskrunner/shell"
17
        "github.com/samsarahq/taskrunner/watcher"
18
        "go.uber.org/multierr"
19
        "golang.org/x/sync/errgroup"
20
        "mvdan.cc/sh/interp"
21
)
22

23
// Executor constructs and executes a DAG for the tasks specified and
24
// desired. It maintains the state of execution for individual tasks,
25
// accepts invalidation events, and schedules re-executions when
26
// necessary.
27
type Executor struct {
28
        ctx    context.Context
29
        config *config.Config
30

31
        // tasks is set of desired tasks to be evaluated in the executor DAG.
32
        tasks taskSet
33

34
        // watchMode is whether or not executor.Run should watch for file changes.
35
        watchMode bool
36

37
        // mu locks on executor evaluations, preventing
38
        // multiple plans or multiple passes from running concurrently.
39
        mu sync.Mutex
40

41
        // wg blocks until the executor has completed all tasks.
42
        wg errgroup.Group
43

44
        // invalidationCh is used to coalesce incoming invalidations into
45
        // a single event.
46
        invalidationCh chan struct{}
47

48
        // taskRegistry contains all available tasks registered to the executor,
49
        // whether or not they are desired.
50
        taskRegistry map[string]*Task
51

52
        // taskFlagsRegistry contains all supported flags per task
53
        // registered to the executor, whether or not they are desired.
54
        taskFlagsRegistry map[string]map[string]TaskFlag
55

56
        // taskFlagArgs contains all desired options grouped by desired tasks
57
        // passed into CLI.
58
        taskFlagArgs map[string][]string
59

60
        shellRunOptions []shell.RunOption
61

62
        // eventsCh keeps track of subscribers to events for this executor.
63
        eventsChs []chan ExecutorEvent
64

65
        // watcherEnhancers are enhancer functions to replace the default watcher.
66
        watcherEnhancers []WatcherEnhancer
67
}
68

69
// WatcherEnhancer is a function to modify or replace a watcher.
70
type WatcherEnhancer func(watcher.Watcher) watcher.Watcher
71

72
var errUndefinedTaskName = errors.New("undefined task name")
73

74
type ExecutorOption func(*Executor)
75

76
const helpMsgTemplate = `
77
⭐️ {{.TaskName}}
78
{{if .TaskDescription}}
79
  {{.TaskDescription}}{{end}}
80
{{if eq (len .Flags) 0}}
81
  No flags are supported.
82
{{else}}
83
  The following flags are supported:
84
  {{range $flag := .Flags}}
85
  ⛳️ {{if .LongName}}--{{.LongName}}{{end}}{{if .ShortName}}{{if .LongName}} | {{end}}-{{rtos .ShortName}}{{end}} [{{.ValueType}}]{{if not (eq .Default "")}} (Default Value: {{.Default}}){{end}}
86
    {{.Description}}
87
{{end}}
88
{{end}}
89
`
90

91
var flagTypeToGetter = map[string]string{
92
        StringTypeFlag:   "StringVal",
93
        BoolTypeFlag:     "BoolVal",
94
        IntTypeFlag:      "IntVal",
95
        Float64TypeFlag:  "Float64Val",
96
        DurationTypeFlag: "DurationVal",
97
}
98

99
// WithWatcherEnhancer adds a watcher enhancer to run when creating the enhancer.
100
func WithWatcherEnhancer(we WatcherEnhancer) ExecutorOption {
×
101
        return func(e *Executor) {
×
102
                e.watcherEnhancers = append(e.watcherEnhancers, we)
×
103
        }
×
104
}
105

106
// WithWatchMode controls the file watching mode.
107
func WithWatchMode(watchMode bool) ExecutorOption {
2✔
108
        return func(e *Executor) {
4✔
109
                e.watchMode = watchMode
2✔
110
        }
2✔
111
}
112

113
func ShellRunOptions(opts ...shell.RunOption) ExecutorOption {
×
114
        return func(e *Executor) {
×
115
                e.shellRunOptions = append(e.shellRunOptions, opts...)
×
116
        }
×
117
}
118

119
// NewExecutor initializes a new executor.
120
func NewExecutor(config *config.Config, tasks []*Task, opts ...ExecutorOption) *Executor {
20✔
121
        executor := &Executor{
20✔
122
                config:            config,
20✔
123
                invalidationCh:    make(chan struct{}, 1),
20✔
124
                taskRegistry:      make(map[string]*Task),
20✔
125
                taskFlagsRegistry: make(map[string]map[string]TaskFlag),
20✔
126
                taskFlagArgs:      make(map[string][]string),
20✔
127
        }
20✔
128

20✔
129
        for _, opt := range opts {
52✔
130
                opt(executor)
32✔
131
        }
32✔
132

133
        for _, task := range tasks {
43✔
134
                executor.taskRegistry[task.Name] = task
23✔
135
        }
23✔
136

137
        return executor
20✔
138
}
139

140
// Config returns the taskrunner configuration.
141
func (e *Executor) Config() *config.Config { return e.config }
×
142

143
// Subscribe returns a channel of executor-level events. Each invocation
144
// of Events() returns a new channel. The done function should be called
145
// to unregister this channel.
146
func (e *Executor) Subscribe() (events <-chan ExecutorEvent) {
4✔
147
        ch := make(chan ExecutorEvent, 1024)
4✔
148
        e.eventsChs = append(e.eventsChs, ch)
4✔
149
        return ch
4✔
150
}
4✔
151

152
func (e *Executor) publishEvent(event ExecutorEvent) {
30✔
153
        for _, ch := range e.eventsChs {
52✔
154
                ch <- event
22✔
155
        }
22✔
156
}
157

158
// runInvalidationLoop kicks off a background goroutine that plans and
159
// runs re-executions after invalidations occur. It coalesces invalidations
160
// every second.
161
func (e *Executor) runInvalidationLoop() {
7✔
162
        timer := time.NewTimer(time.Second)
7✔
163
        timer.Stop()
7✔
164

7✔
165
        go func() {
14✔
166
                for {
18✔
167
                        select {
11✔
168
                        case <-e.invalidationCh:
2✔
169
                                timer.Reset(time.Second)
2✔
170

171
                        case <-e.ctx.Done():
3✔
172
                                return
3✔
173

174
                        case <-timer.C:
2✔
175
                                e.evaluateInvalidationPlan()
2✔
176
                                go e.runPass()
2✔
177
                        }
178
                }
179
        }()
180
}
181

182
// evaluateInvalidationPlan find all tasks that have pending invalidations
183
// and kicks off their re-execution.
184
func (e *Executor) evaluateInvalidationPlan() {
13✔
185
        // Wait for potential side effects from the last evaluation to complete.
13✔
186
        // For instance, if task A depends on task B and task B changes a file that
13✔
187
        // task A needs, then we want to wait for the file events from task B to propagate
13✔
188
        // before evaluating the new plan. We must rely on timing because we cannot
13✔
189
        // follow and wait for the execution to come back through fswatch.
13✔
190
        time.Sleep(time.Millisecond * 1000)
13✔
191
        e.mu.Lock()
13✔
192
        defer e.mu.Unlock()
13✔
193

13✔
194
        var toInvalidate []*taskExecution
13✔
195
        for _, execution := range e.tasks {
37✔
196
                if len(execution.pendingInvalidations) == 0 || execution.state == taskExecutionState_invalid {
45✔
197
                        continue
21✔
198
                }
199

200
                var reasons []InvalidationEvent
3✔
201
                for reason := range execution.pendingInvalidations {
6✔
202
                        reasons = append(reasons, reason)
3✔
203
                }
3✔
204

205
                e.publishEvent(&TaskInvalidatedEvent{
3✔
206
                        simpleEvent: execution.simpleEvent(),
3✔
207
                        Reasons:     reasons,
3✔
208
                })
3✔
209

3✔
210
                toInvalidate = append(toInvalidate, execution)
3✔
211
        }
212

213
        for _, execution := range toInvalidate {
16✔
214
                execution.invalidate(e.ctx)
3✔
215
        }
3✔
216
}
217

218
// Invalidate marks a task and its dependencies as invalidated. If any
219
// tasks become invalidated from this call, Invalidate() will also
220
// schedule a re-execution of the DAG.
221
func (e *Executor) Invalidate(task *Task, event InvalidationEvent) {
2✔
222
        execution := e.tasks[task]
2✔
223
        if didInvalidate := execution.Invalidate(event); !didInvalidate {
2✔
224
                return
×
225
        }
×
226

227
        e.invalidationCh <- struct{}{}
2✔
228
}
229

230
func (e *Executor) Run(ctx context.Context, taskNames []string, runtime *Runtime) error {
7✔
231
        e.ctx = ctx
7✔
232
        defer func() {
14✔
233
                for _, ch := range e.eventsChs {
11✔
234
                        close(ch)
4✔
235
                }
4✔
236
        }()
237
        e.runInvalidationLoop()
7✔
238
        if e.watchMode {
8✔
239
                e.runWatch(ctx)
1✔
240
        }
1✔
241

242
        // Build up the DAG for task executions.
243
        taskSet := make(taskSet)
7✔
244
        for _, taskName := range taskNames {
14✔
245
                task := e.taskRegistry[taskName]
7✔
246
                if task == nil {
7✔
247
                        return oops.Wrapf(errUndefinedTaskName, "task %s is not defined", taskName)
×
248
                }
×
249
                taskSet.add(ctx, task)
7✔
250
        }
251

252
        e.tasks = taskSet
7✔
253

7✔
254
        // If "--help/-h" is passed to any of the desired tasks, generate and show
7✔
255
        // help text without actually running any tasks.
7✔
256
        var tasksWithHelpOption []string
7✔
257
        for task := range e.tasks {
17✔
258
                for _, optionArg := range e.taskFlagArgs[task.Name] {
10✔
259
                        if optionArg == "-h" || optionArg == "--help" {
×
260
                                tasksWithHelpOption = append(tasksWithHelpOption, task.Name)
×
261
                                break
×
262
                        }
263
                }
264
        }
265

266
        if len(tasksWithHelpOption) != 0 {
7✔
267
                for _, task := range tasksWithHelpOption {
×
268
                        e.showTaskFlagHelpText(task)
×
269
                }
×
270
                return nil
×
271
        }
272

273
        var errors error
7✔
274
        // Run all onStartHooks before starting, after the DAG has been created.
7✔
275
        for i, hook := range runtime.onStartHooks {
7✔
276
                if err := hook(ctx, e); err != nil {
×
NEW
277
                        errors = multierr.Append(errors, oops.Wrapf(err, "OnStart hook %d failed", i))
×
278
                }
×
279
        }
280
        if errors != nil {
7✔
281
                return errors
×
282
        }
×
283

284
        e.runPass()
7✔
285

7✔
286
        // Wait on all tasks to exit before stopping.
7✔
287
        errors = multierr.Append(errors, e.wg.Wait())
7✔
288

7✔
289
        // Run all onStopHooks after stopping.
7✔
290
        for i, hook := range runtime.onStopHooks {
7✔
291
                if err := hook(ctx, e); err != nil {
×
NEW
292
                        errors = multierr.Append(errors, oops.Wrapf(err, "OnStop hook %d failed", i))
×
293
                }
×
294
        }
295

296
        return errors
7✔
297
}
298

299
// ShellRun executes a shell.Run with some default options:
300
// Commands for tasks are automatically logged (stderr and stdout are forwarded).
301
// Commands run in a consistent environment (configurable on a taskrunner level).
302
// Commands run in taskrunner's working directory.
303
func (e *Executor) ShellRun(ctx context.Context, command string, opts ...shell.RunOption) error {
1✔
304
        options := []shell.RunOption{
1✔
305
                func(r *interp.Runner) {
2✔
306
                        logger := LoggerFromContext(ctx)
1✔
307
                        if logger == nil {
1✔
308
                                return
×
309
                        }
×
310

311
                        r.Stdout = logger.Stdout
1✔
312
                        r.Stderr = logger.Stderr
1✔
313
                },
314
        }
315
        options = append(options, e.shellRunOptions...)
1✔
316
        options = append(options, opts...)
1✔
317
        err := shell.Run(ctx, command, options...)
1✔
318
        if err != nil {
2✔
319
                return oops.Wrapf(err, "Executor failed to run shell command")
1✔
320
        }
1✔
321
        return nil
×
322
}
323

324
func (e *Executor) taskExecution(t *Task) *taskExecution { return e.tasks[t] }
1✔
325
func (e *Executor) provideEventLogger(t *Task) *Logger {
13✔
326
        stderr := &eventLogger{
13✔
327
                executor: e,
13✔
328
                task:     t,
13✔
329
                stream:   TaskLogEventStderr,
13✔
330
        }
13✔
331
        stdout := *stderr
13✔
332
        stdout.stream = TaskLogEventStdout
13✔
333
        return &Logger{
13✔
334
                Stderr: stderr,
13✔
335
                Stdout: &stdout,
13✔
336
        }
13✔
337
}
13✔
338

339
func (e *Executor) getDefaultTaskFlagMap(taskName string) map[string]FlagArg {
14✔
340
        supportedTaskFlags := e.taskFlagsRegistry[taskName]
14✔
341
        defaultTaskFlagsMap := make(map[string]FlagArg)
14✔
342

14✔
343
        for key, flag := range supportedTaskFlags {
112✔
344
                keyCopy := key
98✔
345
                flagCopy := flag
98✔
346
                flagValErrMsg := fmt.Sprintf("The type for the `%s` flag is `%s`. Please use `%s`", keyCopy, flag.ValueType, flagTypeToGetter[flag.ValueType])
98✔
347
                flagArg := FlagArg{
98✔
348
                        Value: nil,
98✔
349
                        BoolVal: func() *bool {
111✔
350
                                if flagCopy.ValueType != BoolTypeFlag {
20✔
351
                                        panic(flagValErrMsg)
7✔
352
                                }
353

354
                                if flagCopy.Default != "" {
10✔
355
                                        boolVal, err := strconv.ParseBool(flagCopy.Default)
4✔
356
                                        if err != nil {
4✔
357
                                                panic(fmt.Sprintf("Please pass a bool as the value. Err: %s", err))
×
358
                                        }
359

360
                                        return &boolVal
4✔
361
                                }
362

363
                                return nil
2✔
364
                        },
365
                        IntVal: func() *int {
11✔
366
                                if flagCopy.ValueType != IntTypeFlag {
20✔
367
                                        panic(flagValErrMsg)
9✔
368
                                }
369

370
                                if flagCopy.Default != "" {
4✔
371
                                        intVal, err := strconv.Atoi(flagCopy.Default)
2✔
372
                                        if err != nil {
2✔
373
                                                panic(fmt.Sprintf("Please pass an int as the value. Err: %s", err))
×
374
                                        }
375
                                        return &intVal
2✔
376
                                }
377

378
                                return nil
×
379
                        },
380
                        Float64Val: func() *float64 {
11✔
381
                                if flagCopy.ValueType != Float64TypeFlag {
20✔
382
                                        panic(flagValErrMsg)
9✔
383
                                }
384

385
                                if flagCopy.Default != "" {
4✔
386
                                        float64Val, err := strconv.ParseFloat(flagCopy.Default, 64)
2✔
387
                                        if err != nil {
2✔
388
                                                panic(fmt.Sprintf("Please pass a float64 as the value. Err: %s", err))
×
389
                                        }
390
                                        return &float64Val
2✔
391
                                }
392

393
                                return nil
×
394
                        },
395
                        DurationVal: func() *time.Duration {
11✔
396
                                if flagCopy.ValueType != DurationTypeFlag {
21✔
397
                                        panic(flagValErrMsg)
10✔
398
                                }
399

400
                                if flagCopy.Default != "" {
2✔
401
                                        duration, err := time.ParseDuration(flagCopy.Default)
1✔
402
                                        if err != nil {
1✔
403
                                                panic(fmt.Sprintf("Please pass a duration as the value. Err: %s", err))
×
404
                                        }
405
                                        return &duration
1✔
406
                                }
407

408
                                return nil
×
409
                        },
410
                        StringVal: func() *string {
11✔
411
                                if flagCopy.ValueType != StringTypeFlag {
20✔
412
                                        panic(flagValErrMsg)
9✔
413
                                }
414

415
                                if flagCopy.Default != "" {
4✔
416
                                        return &flagCopy.Default
2✔
417
                                }
2✔
418

419
                                return nil
×
420
                        },
421
                }
422

423
                defaultTaskFlagsMap[key] = flagArg
98✔
424
        }
425

426
        return defaultTaskFlagsMap
14✔
427
}
428

429
func (e *Executor) parseTaskFlagsIntoMap(taskName string, flags []string) map[string]FlagArg {
14✔
430
        taskFlagsMap := e.getDefaultTaskFlagMap(taskName)
14✔
431

14✔
432
        for _, flag := range flags {
33✔
433
                var key string
19✔
434
                var val string
19✔
435
                splitFlag := strings.Split(flag, "=")
19✔
436

19✔
437
                key, err := e.getVerifiedFlagKey(taskName, splitFlag[0])
19✔
438
                if err != nil {
20✔
439
                        panic(fmt.Sprintf("Unsupported flag passed to %s: `%s`. See error: %s", taskName, key, err))
1✔
440
                }
441

442
                if len(splitFlag) > 2 || len(splitFlag) == 0 {
19✔
443
                        // If the passed flag has >1 "=" in it or the arg was an empty string,
1✔
444
                        // then the flag has invalid syntax.
1✔
445
                        panic(fmt.Sprintf("Invalid flag syntax for %s: `%s`", taskName, flag))
1✔
446
                } else if len(splitFlag) == 2 {
28✔
447
                        // If the passed flag has an "=" in it, assume that variable flag was set
11✔
448
                        // (e.g. --var="val").
11✔
449
                        val = splitFlag[1]
11✔
450
                }
11✔
451

452
                // At this point, we should have verified that this flag is supported in `getVerifiedFlagKey`.
453
                taskFlag := e.taskFlagsRegistry[taskName][key]
17✔
454
                flagValErrMsg := fmt.Sprintf("The type for the `%s` flag is `%s`. Please use `%s`", key, taskFlag.ValueType, flagTypeToGetter[taskFlag.ValueType])
17✔
455

17✔
456
                // If no val was passed, use the default flagArg that is already in the taskFlagsMap.
17✔
457
                if val != "" {
28✔
458
                        flagArg := FlagArg{
11✔
459
                                // Return the raw string val passed.
11✔
460
                                Value: val,
11✔
461
                                BoolVal: func() *bool {
19✔
462
                                        if taskFlag.ValueType != BoolTypeFlag {
16✔
463
                                                panic(flagValErrMsg)
8✔
464
                                        }
465

466
                                        // Support flags that are passed either like `--flag=true` or `--flag="true"`.
467
                                        strippedKey := stripWrappingQuotations(val)
×
468
                                        parsedBool, err := strconv.ParseBool(strippedKey)
×
469
                                        if err != nil {
×
470
                                                panic(fmt.Sprintf("Please pass a bool as the value. Err: %s", err))
×
471
                                        }
472

473
                                        return &parsedBool
×
474
                                },
475
                                DurationVal: func() *time.Duration {
8✔
476
                                        if taskFlag.ValueType != DurationTypeFlag {
14✔
477
                                                panic(flagValErrMsg)
6✔
478
                                        }
479

480
                                        // Support flags that are passed either like `--flag=100ms` or `--flag="100ms"`.
481
                                        strippedKey := stripWrappingQuotations(val)
2✔
482
                                        duration, err := time.ParseDuration(strippedKey)
2✔
483
                                        if err != nil {
2✔
484
                                                panic(fmt.Sprintf("Please pass a duration as the value. Err: %s", err))
×
485
                                        }
486

487
                                        return &duration
2✔
488
                                },
489
                                IntVal: func() *int {
8✔
490
                                        if taskFlag.ValueType != IntTypeFlag {
14✔
491
                                                panic(flagValErrMsg)
6✔
492
                                        }
493

494
                                        // Support flags that are passed either like `--flag=1` or `--flag="1"`.
495
                                        strippedKey := stripWrappingQuotations(val)
2✔
496
                                        int, err := strconv.Atoi(strippedKey)
2✔
497
                                        if err != nil {
2✔
498
                                                panic(fmt.Sprintf("Please pass an int as the value. Err: %s", err))
×
499
                                        }
500
                                        return &int
2✔
501
                                },
502
                                Float64Val: func() *float64 {
8✔
503
                                        if taskFlag.ValueType != Float64TypeFlag {
14✔
504
                                                panic(flagValErrMsg)
6✔
505
                                        }
506

507
                                        // Support flags that are passed either like `--flag=1.3` or `--flag="1.3"`.
508
                                        strippedKey := stripWrappingQuotations(val)
2✔
509

2✔
510
                                        float, err := strconv.ParseFloat(strippedKey, 64)
2✔
511
                                        if err != nil {
2✔
512
                                                panic(fmt.Sprintf("Please pass a float64 as the value. Err: %s", err))
×
513
                                        }
514
                                        return &float
2✔
515
                                },
516
                                StringVal: func() *string {
8✔
517
                                        if taskFlag.ValueType != StringTypeFlag {
14✔
518
                                                panic(flagValErrMsg)
6✔
519
                                        }
520

521
                                        // Support flags that are passed either like `--flag=val` or `--flag="val"`.
522
                                        strippedStr := stripWrappingQuotations(val)
2✔
523
                                        return &strippedStr
2✔
524
                                },
525
                        }
526

527
                        taskFlagsMap[key] = flagArg
11✔
528
                        // Allow readers to index into flag map via either LongName or ShortName
11✔
529
                        // regardless of which arg was passed in if both names are available.
11✔
530
                        if len(key) == 1 && key != taskFlag.LongName && taskFlag.LongName != "" {
11✔
531
                                // If only one char was passed through, check whether a LongName is available
×
532
                                // and also register it in the map.
×
533
                                key = taskFlag.LongName
×
534
                                taskFlagsMap[key] = flagArg
×
535
                        } else if len(key) > 1 && key == taskFlag.LongName && taskFlag.ShortName != 0 {
11✔
536
                                // If >1 char was passed through, check whether a ShortName is available
×
537
                                // and also register it in the map.
×
538
                                key = string(taskFlag.ShortName)
×
539
                                taskFlagsMap[key] = flagArg
×
540
                        }
×
541
                }
542
        }
543

544
        return taskFlagsMap
12✔
545
}
546

547
func stripWrappingQuotations(str string) string {
8✔
548
        strippedVal := str
8✔
549
        if len(str) >= 2 && strings.HasPrefix(str, "\"") && strings.HasSuffix(str, "\"") {
12✔
550
                strippedVal = str[1 : len(str)-1]
4✔
551
        }
4✔
552

553
        return strippedVal
8✔
554
}
555

556
func (e *Executor) getVerifiedFlagKey(taskName string, flagKey string) (string, error) {
19✔
557
        var strippedKey string
19✔
558
        if strings.HasPrefix(flagKey, "--") {
20✔
559
                // If the flag key is prefixed with "--", we expect it to be the flag LongName.
1✔
560
                strippedKey = string(flagKey[2:])
1✔
561
                if len(strippedKey) <= 1 {
1✔
562
                        return flagKey, errors.New(fmt.Sprintf("Unknown flag: `%s`", strippedKey))
×
563
                }
×
564
        } else if strings.HasPrefix(flagKey, "-") {
36✔
565
                // If the flag key is prefixed with "-", we expect it to be the flag ShortName.
18✔
566
                strippedKey = string(flagKey[1:])
18✔
567
                if len(strippedKey) != 1 {
18✔
568
                        return flagKey, errors.New(fmt.Sprintf("Did you mean `--%s` (with two dashes)?", strippedKey))
×
569
                }
×
570
        } else {
×
571
                // If the flag is not prefixed with any dashes, this is invalid syntax.
×
572
                return flagKey, errors.New(fmt.Sprintf("LongName flags must be prefixed with `--`. ShortName flags must be prefixed with `-`"))
×
573
        }
×
574

575
        // Check that task is valid.
576
        if _, ok := e.taskFlagsRegistry[taskName]; ok {
38✔
577
                // Check that flag is supported.
19✔
578
                if _, ok = e.taskFlagsRegistry[taskName][strippedKey]; ok {
37✔
579
                        return strippedKey, nil
18✔
580
                }
18✔
581
        }
582

583
        return flagKey, errors.New(fmt.Sprintf("Unsupported flag: %s", flagKey))
1✔
584
}
585

586
func (e *Executor) showTaskFlagHelpText(taskName string) {
×
587
        task := e.taskRegistry[taskName]
×
588
        taskFlags := task.Flags
×
589
        helpTemplate := template.New("helpText")
×
590
        helpTemplate = helpTemplate.Funcs(template.FuncMap{
×
591
                "rtos": func(r rune) string { return string(r) },
×
592
        })
593
        helpTemplate, err := helpTemplate.Parse(helpMsgTemplate)
×
594
        if err != nil {
×
595
                fmt.Printf("There was an error generating the help text: %s", err)
×
596
        }
×
597

598
        err = helpTemplate.Execute(os.Stdout, struct {
×
599
                TaskName        string
×
600
                TaskDescription string
×
601
                Flags           []TaskFlag
×
602
        }{
×
603
                TaskName:        taskName,
×
604
                TaskDescription: task.Description,
×
605
                Flags:           taskFlags,
×
606
        })
×
607
        if err != nil {
×
608
                fmt.Printf("There was an error generating the help text: %s", err)
×
609
        }
×
610
}
611

612
// runPass kicks off tasks that are in an executable state.
613
func (e *Executor) runPass() {
22✔
614
        if e.ctx.Err() != nil {
26✔
615
                return
4✔
616
        }
4✔
617

618
        e.mu.Lock()
18✔
619
        defer e.mu.Unlock()
18✔
620

18✔
621
        for task, execution := range e.tasks {
46✔
622
                if execution.ShouldExecute() {
41✔
623
                        execution.state = taskExecutionState_running
13✔
624

13✔
625
                        func(task *Task, execution *taskExecution) {
26✔
626
                                e.wg.Go(func() error {
26✔
627
                                        logger := e.provideEventLogger(task)
13✔
628

13✔
629
                                        ctx := context.WithValue(execution.ctx, loggerKey{}, logger)
13✔
630

13✔
631
                                        e.publishEvent(&TaskStartedEvent{
13✔
632
                                                simpleEvent: execution.simpleEvent(),
13✔
633
                                        })
13✔
634

13✔
635
                                        started := time.Now()
13✔
636
                                        var duration time.Duration
13✔
637
                                        var err error
13✔
638

13✔
639
                                        if task.RunWithFlags != nil {
14✔
640
                                                taskFlagsMap := make(map[string]FlagArg)
1✔
641

1✔
642
                                                if passedFlags, ok := e.taskFlagArgs[task.Name]; ok {
1✔
643
                                                        taskFlagsMap = e.parseTaskFlagsIntoMap(task.Name, passedFlags)
×
644
                                                }
×
645
                                                err = task.RunWithFlags(ctx, e.ShellRun, taskFlagsMap)
1✔
646
                                                duration = time.Since(started)
1✔
647
                                        } else if task.Run != nil {
24✔
648
                                                err = task.Run(ctx, e.ShellRun)
12✔
649
                                                duration = time.Since(started)
12✔
650
                                        }
12✔
651

652
                                        if ctx.Err() == context.Canceled {
13✔
653
                                                // Only move ourselves to permanently canceled if taskrunner is shutting down. Note
×
654
                                                // that the invalidation codepath already set the state as invalid, so there is
×
655
                                                // no else statement.
×
656
                                                if e.ctx.Err() != nil {
×
657
                                                        execution.state = taskExecutionState_canceled
×
658
                                                }
×
659
                                                e.publishEvent(&TaskStoppedEvent{
×
660
                                                        simpleEvent: execution.simpleEvent(),
×
661
                                                })
×
662
                                        } else if err != nil {
15✔
663
                                                execution.state = taskExecutionState_error
2✔
664
                                                e.publishEvent(&TaskFailedEvent{
2✔
665
                                                        simpleEvent: execution.simpleEvent(),
2✔
666
                                                        Error:       err,
2✔
667
                                                })
2✔
668
                                        } else {
13✔
669
                                                execution.state = taskExecutionState_done
11✔
670
                                                e.publishEvent(&TaskCompletedEvent{
11✔
671
                                                        simpleEvent: execution.simpleEvent(),
11✔
672
                                                        Duration:    duration,
11✔
673
                                                })
11✔
674
                                        }
11✔
675

676
                                        // It's important that we flush the error/done states before
677
                                        // terminating the channel. It's also important that possible
678
                                        // invalidations occur after exit so that those channels do not block,
679
                                        // waiting for this to complete.
680
                                        execution.terminalCh <- struct{}{}
13✔
681

13✔
682
                                        if task.KeepAlive && execution.state == taskExecutionState_error {
13✔
683
                                                e.Invalidate(task, KeepAliveStopped{})
×
684
                                        }
×
685

686
                                        if err == nil {
24✔
687
                                                e.evaluateInvalidationPlan()
11✔
688
                                        }
11✔
689

690
                                        e.runPass()
13✔
691
                                        if err != nil && ctx.Err() != context.Canceled {
15✔
692
                                                return oops.Wrapf(err, "Executor failed to run task")
2✔
693
                                        }
2✔
694

695
                                        return nil
11✔
696
                                })
697
                        }(task, execution)
698

699
                }
700
        }
701
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc