From d1daf2f28d8c2256e5712e3064ce6ac45fca2562 Mon Sep 17 00:00:00 2001 From: Alex Savchuk <a.u.savchuk@gmail.com> Date: Thu, 8 Sep 2022 17:20:39 +0300 Subject: [PATCH] fix: support expression for step's continue-on-error field (#900) (#1331) Co-authored-by: Markus Wolf <KnisterPeter@users.noreply.github.com> --- pkg/model/workflow.go | 22 ++++++------- pkg/runner/step.go | 25 ++++++++++++++- pkg/runner/step_test.go | 70 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 53b1cb2..86ec67f 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -350,17 +350,17 @@ type ContainerSpec struct { // Step is the structure of one step in a job type Step struct { - ID string `yaml:"id"` - If yaml.Node `yaml:"if"` - Name string `yaml:"name"` - Uses string `yaml:"uses"` - Run string `yaml:"run"` - WorkingDirectory string `yaml:"working-directory"` - Shell string `yaml:"shell"` - Env yaml.Node `yaml:"env"` - With map[string]string `yaml:"with"` - ContinueOnError bool `yaml:"continue-on-error"` - TimeoutMinutes string `yaml:"timeout-minutes"` + ID string `yaml:"id"` + If yaml.Node `yaml:"if"` + Name string `yaml:"name"` + Uses string `yaml:"uses"` + Run string `yaml:"run"` + WorkingDirectory string `yaml:"working-directory"` + Shell string `yaml:"shell"` + Env yaml.Node `yaml:"env"` + With map[string]string `yaml:"with"` + RawContinueOnError string `yaml:"continue-on-error"` + TimeoutMinutes string `yaml:"timeout-minutes"` } // String gets the name of step diff --git a/pkg/runner/step.go b/pkg/runner/step.go index cf403b6..5b37ce3 100644 --- a/pkg/runner/step.go +++ b/pkg/runner/step.go @@ -99,7 +99,14 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo logger.WithField("stepResult", rc.StepResults[rc.CurrentStep].Outcome).Infof(" \u2705 Success - %s %s", stage, stepString) } else { rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure - if stepModel.ContinueOnError { + + continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage) + if parseErr != nil { + rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure + return parseErr + } + + if continueOnError { logger.Infof("Failed but continue next step") err = nil rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess @@ -183,6 +190,22 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage) return runStep, nil } +func isContinueOnError(ctx context.Context, expr string, step step, stage stepStage) (bool, error) { + // https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=true#L962 + if len(strings.TrimSpace(expr)) == 0 { + return false, nil + } + + rc := step.getRunContext() + + continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone) + if err != nil { + return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err) + } + + return continueOnError, nil +} + func mergeIntoMap(target *map[string]string, maps ...map[string]string) { for _, m := range maps { for k, v := range m { diff --git a/pkg/runner/step_test.go b/pkg/runner/step_test.go index cfce24e..621f873 100644 --- a/pkg/runner/step_test.go +++ b/pkg/runner/step_test.go @@ -276,3 +276,73 @@ func TestIsStepEnabled(t *testing.T) { } assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain)) } + +func TestIsContinueOnError(t *testing.T) { + createTestStep := func(t *testing.T, input string) step { + var step *model.Step + err := yaml.Unmarshal([]byte(input), &step) + assert.NoError(t, err) + + return &stepRun{ + RunContext: &RunContext{ + Config: &Config{ + Workdir: ".", + Platforms: map[string]string{ + "ubuntu-latest": "ubuntu-latest", + }, + }, + StepResults: map[string]*model.StepResult{}, + Env: map[string]string{}, + Run: &model.Run{ + JobID: "job1", + Workflow: &model.Workflow{ + Name: "workflow1", + Jobs: map[string]*model.Job{ + "job1": createJob(t, `runs-on: ubuntu-latest`, ""), + }, + }, + }, + }, + Step: step, + } + } + + log.SetLevel(log.DebugLevel) + assertObject := assert.New(t) + + // absent + step := createTestStep(t, "name: test") + continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.False(continueOnError) + assertObject.Nil(err) + + // explcit true + step = createTestStep(t, "continue-on-error: true") + continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.True(continueOnError) + assertObject.Nil(err) + + // explicit false + step = createTestStep(t, "continue-on-error: false") + continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.False(continueOnError) + assertObject.Nil(err) + + // expression true + step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}") + continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.True(continueOnError) + assertObject.Nil(err) + + // expression false + step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}") + continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.False(continueOnError) + assertObject.Nil(err) + + // expression parse error + step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}") + continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain) + assertObject.False(continueOnError) + assertObject.NotNil(err) +}