act/pkg/common/executor.go
Markus Wolf 1891c72ab1
fix: continue jobs + steps after failure (#840)
* fix: continue jobs + steps after failure

To allow proper if expression handling on jobs and steps (like always,
success, failure, ...) we need to continue running all executors in
the prepared chain.
To keep the error handling intact we add an occurred error to the
go context and handle it later in the pipeline/chain.

Also we add the job result to the needs context to give expressions
access to it.
The needs object, failure and success functions are split between
run context (on jobs) and step context.

Closes #442

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* style: correct linter warnings

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* fix: job if value defaults to success()

As described in the documentation, a default value of "success()" is
applied when no "if" value is present on the job.

https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions

Co-authored-by: Markus Wolf <mail@markus-wolf.de>

* fix: check job needs recursively

Ensure job result includes results of previous jobs

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* test: add runner test for job status check functions

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* test: add unit tests for run context if evaluation

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* refactor: move if expression evaluation

Move if expression evaluation into own function (step context) to
better support unit testing.

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* test: add unit tests for step context if evaluation

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* fix: handle job error more resilient

The job error is not stored in a context map instead of a context
added value.
Since context values are immutable an added value requires to keep
the new context in all cases. This is fragile since it might slip
unnoticed to other parts of the code.

Storing the error of a job in the context map will make it more stable,
since the map is always there and the context of the pipeline is stable
for the whole run.

* feat: steps should use a default if expression of success()

* test: add integration test for if-expressions

* chore: disable editorconfig-checker for yaml multiline string

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>
Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 20:57:42 +00:00

193 lines
4.3 KiB
Go

package common
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
)
// Warning that implements `error` but safe to ignore
type Warning struct {
Message string
}
// Error the contract for error
func (w Warning) Error() string {
return w.Message
}
// Warningf create a warning
func Warningf(format string, args ...interface{}) Warning {
w := Warning{
Message: fmt.Sprintf(format, args...),
}
return w
}
// Executor define contract for the steps of a workflow
type Executor func(ctx context.Context) error
// Conditional define contract for the conditional predicate
type Conditional func(ctx context.Context) bool
// NewInfoExecutor is an executor that logs messages
func NewInfoExecutor(format string, args ...interface{}) Executor {
return func(ctx context.Context) error {
logger := Logger(ctx)
logger.Infof(format, args...)
return nil
}
}
// NewDebugExecutor is an executor that logs messages
func NewDebugExecutor(format string, args ...interface{}) Executor {
return func(ctx context.Context) error {
logger := Logger(ctx)
logger.Debugf(format, args...)
return nil
}
}
// NewPipelineExecutor creates a new executor from a series of other executors
func NewPipelineExecutor(executors ...Executor) Executor {
if len(executors) == 0 {
return func(ctx context.Context) error {
return nil
}
}
var rtn Executor
for _, executor := range executors {
if rtn == nil {
rtn = executor
} else {
rtn = rtn.Then(executor)
}
}
return rtn
}
// NewConditionalExecutor creates a new executor based on conditions
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
return func(ctx context.Context) error {
if conditional(ctx) {
if trueExecutor != nil {
return trueExecutor(ctx)
}
} else {
if falseExecutor != nil {
return falseExecutor(ctx)
}
}
return nil
}
}
// NewErrorExecutor creates a new executor that always errors out
func NewErrorExecutor(err error) Executor {
return func(ctx context.Context) error {
return err
}
}
// NewParallelExecutor creates a new executor from a parallel of other executors
func NewParallelExecutor(executors ...Executor) Executor {
return func(ctx context.Context) error {
errChan := make(chan error)
for _, executor := range executors {
e := executor
go func() {
err := e.ChannelError(errChan)(ctx)
if err != nil {
log.Fatal(err)
}
}()
}
// Executor waits all executors to cleanup these resources.
var firstErr error
for i := 0; i < len(executors); i++ {
if err := <-errChan; err != nil && firstErr == nil {
firstErr = err
}
}
if err := ctx.Err(); err != nil {
return err
}
return firstErr
}
}
// ChannelError sends error to errChan rather than returning error
func (e Executor) ChannelError(errChan chan error) Executor {
return func(ctx context.Context) error {
errChan <- e(ctx)
return nil
}
}
// Then runs another executor if this executor succeeds
func (e Executor) Then(then Executor) Executor {
return func(ctx context.Context) error {
err := e(ctx)
if err != nil {
switch err.(type) {
case Warning:
log.Warning(err.Error())
default:
SetJobError(ctx, err)
}
} else if ctx.Err() != nil {
SetJobError(ctx, ctx.Err())
}
return then(ctx)
}
}
// If only runs this executor if conditional is true
func (e Executor) If(conditional Conditional) Executor {
return func(ctx context.Context) error {
if conditional(ctx) {
return e(ctx)
}
return nil
}
}
// IfNot only runs this executor if conditional is true
func (e Executor) IfNot(conditional Conditional) Executor {
return func(ctx context.Context) error {
if !conditional(ctx) {
return e(ctx)
}
return nil
}
}
// IfBool only runs this executor if conditional is true
func (e Executor) IfBool(conditional bool) Executor {
return e.If(func(ctx context.Context) bool {
return conditional
})
}
// Finally adds an executor to run after other executor
func (e Executor) Finally(finally Executor) Executor {
return func(ctx context.Context) error {
err := e(ctx)
err2 := finally(ctx)
if err2 != nil {
return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
}
return err
}
}
// Not return an inverted conditional
func (c Conditional) Not() Conditional {
return func(ctx context.Context) bool {
return !c(ctx)
}
}