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>
This commit is contained in:
82
pkg/runner/run_context.go
Executable file → Normal file
82
pkg/runner/run_context.go
Executable file → Normal file
@@ -3,7 +3,6 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -288,7 +287,20 @@ func (rc *RunContext) Executor() common.Executor {
|
||||
}
|
||||
steps = append(steps, rc.newStepExecutor(step))
|
||||
}
|
||||
steps = append(steps, rc.stopJobContainer())
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
err := rc.stopJobContainer()(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc.Run.Job().Result = "success"
|
||||
jobError := common.JobError(ctx)
|
||||
if jobError != nil {
|
||||
rc.Run.Job().Result = "failure"
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return common.NewPipelineExecutor(steps...).Finally(rc.interpolateOutputs()).Finally(func(ctx context.Context) error {
|
||||
if rc.JobContainer != nil {
|
||||
@@ -310,15 +322,9 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||
Conclusion: stepStatusSuccess,
|
||||
Outputs: make(map[string]string),
|
||||
}
|
||||
runStep, err := rc.EvalBool(sc.Step.If.Value)
|
||||
|
||||
runStep, err := sc.isEnabled(ctx)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", sc.Step)
|
||||
exprEval, err := sc.setupEnv(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.ExprEval = exprEval
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = stepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Outcome = stepStatusFailure
|
||||
return err
|
||||
@@ -403,7 +409,7 @@ func (rc *RunContext) hostname() string {
|
||||
func (rc *RunContext) isEnabled(ctx context.Context) bool {
|
||||
job := rc.Run.Job()
|
||||
l := common.Logger(ctx)
|
||||
runJob, err := rc.EvalBool(job.If.Value)
|
||||
runJob, err := EvalBool(rc.ExprEval, job.If.Value)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", job.Name)
|
||||
return false
|
||||
@@ -430,51 +436,6 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool {
|
||||
|
||||
var splitPattern *regexp.Regexp
|
||||
|
||||
// EvalBool evaluates an expression against current run context
|
||||
func (rc *RunContext) EvalBool(expr string) (bool, error) {
|
||||
if splitPattern == nil {
|
||||
splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(expr), "!") {
|
||||
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
|
||||
}
|
||||
if expr != "" {
|
||||
parts := splitPattern.FindAllString(expr, -1)
|
||||
var evaluatedParts []string
|
||||
for i, part := range parts {
|
||||
if operatorPattern.MatchString(part) {
|
||||
evaluatedParts = append(evaluatedParts, part)
|
||||
continue
|
||||
}
|
||||
|
||||
interpolatedPart, isString := rc.ExprEval.InterpolateWithStringCheck(part)
|
||||
|
||||
// This peculiar transformation has to be done because the GitHub parser
|
||||
// treats false returned from contexts as a string, not a boolean.
|
||||
// Hence env.SOMETHING will be evaluated to true in an if: expression
|
||||
// regardless if SOMETHING is set to false, true or any other string.
|
||||
// It also handles some other weirdness that I found by trial and error.
|
||||
if (expressionPattern.MatchString(part) && // it is an expression
|
||||
!strings.Contains(part, "!")) && // but it's not negated
|
||||
interpolatedPart == "false" && // and the interpolated string is false
|
||||
(isString || previousOrNextPartIsAnOperator(i, parts)) { // and it's of type string or has an logical operator before or after
|
||||
interpolatedPart = fmt.Sprintf("'%s'", interpolatedPart) // then we have to quote the false expression
|
||||
}
|
||||
|
||||
evaluatedParts = append(evaluatedParts, interpolatedPart)
|
||||
}
|
||||
|
||||
joined := strings.Join(evaluatedParts, " ")
|
||||
v, _, err := rc.ExprEval.Evaluate(fmt.Sprintf("Boolean(%s)", joined))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
log.Debugf("expression '%s' evaluated to '%s'", expr, v)
|
||||
return v == "true", nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func previousOrNextPartIsAnOperator(i int, parts []string) bool {
|
||||
operator := false
|
||||
if i > 0 {
|
||||
@@ -557,6 +518,17 @@ func (rc *RunContext) getStepsContext() map[string]*stepResult {
|
||||
return rc.StepResults
|
||||
}
|
||||
|
||||
func (rc *RunContext) getNeedsTransitive(job *model.Job) []string {
|
||||
needs := job.Needs()
|
||||
|
||||
for _, need := range needs {
|
||||
parentNeeds := rc.getNeedsTransitive(rc.Run.Workflow.GetJob(need))
|
||||
needs = append(needs, parentNeeds...)
|
||||
}
|
||||
|
||||
return needs
|
||||
}
|
||||
|
||||
type githubContext struct {
|
||||
Event map[string]interface{} `json:"event"`
|
||||
EventPath string `json:"event_path"`
|
||||
|
Reference in New Issue
Block a user