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

samsarahq / taskrunner / 14199794123

01 Apr 2025 03:32PM UTC coverage: 54.258% (+2.7%) from 51.517%
14199794123

Pull #72

github

samloop
runner: improve error logging

This clarifies some error messages, and provides the stack trace when taskrunner fails on execution.
Pull Request #72: runner: improve error logging

7 of 8 new or added lines in 3 files covered. (87.5%)

3 existing lines in 1 file now uncovered.

771 of 1421 relevant lines covered (54.26%)

5.01 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 _, hook := range runtime.onStartHooks {
7✔
276
                if err := hook(ctx, e); err != nil {
×
277
                        errors = multierr.Append(errors, err)
×
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 _, hook := range runtime.onStopHooks {
7✔
291
                if err := hook(ctx, e); err != nil {
×
292
                        errors = multierr.Append(errors, err)
×
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✔
NEW
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 {
25✔
615
                return
3✔
616
        }
3✔
617

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

19✔
621
        for task, execution := range e.tasks {
49✔
622
                if execution.ShouldExecute() {
43✔
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