Add more steps context support (#887)

* feat: add more steps context support

- Add step outcome
- Remove step success in favour of conclusion

* feat: add conclusion and outcome steps context tests

Add corresponding tests and extend pkg/runner/expression.go vmSteps() to
handle steps context conclusion and outcome iota as strings.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Victor Popkov 2021-11-27 19:55:26 +02:00 committed by GitHub
parent 6517d04b9a
commit e793d03f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 107 additions and 15 deletions

View File

@ -466,9 +466,19 @@ func (rc *RunContext) vmJob() func(*otto.Otto) {
} }
func (rc *RunContext) vmSteps() func(*otto.Otto) { func (rc *RunContext) vmSteps() func(*otto.Otto) {
steps := rc.getStepsContext() ctxSteps := rc.getStepsContext()
steps := make(map[string]interface{})
for id, ctxStep := range ctxSteps {
steps[id] = map[string]interface{}{
"conclusion": ctxStep.Conclusion.String(),
"outcome": ctxStep.Outcome.String(),
"outputs": ctxStep.Outputs,
}
}
return func(vm *otto.Otto) { return func(vm *otto.Otto) {
log.Debugf("context steps => %v", steps)
_ = vm.Set("steps", steps) _ = vm.Set("steps", steps)
} }
} }

View File

@ -49,22 +49,25 @@ func TestEvaluate(t *testing.T) {
}, },
StepResults: map[string]*stepResult{ StepResults: map[string]*stepResult{
"idwithnothing": { "idwithnothing": {
Conclusion: stepStatusSuccess,
Outcome: stepStatusFailure,
Outputs: map[string]string{ Outputs: map[string]string{
"foowithnothing": "barwithnothing", "foowithnothing": "barwithnothing",
}, },
Success: true,
}, },
"id-with-hyphens": { "id-with-hyphens": {
Conclusion: stepStatusSuccess,
Outcome: stepStatusFailure,
Outputs: map[string]string{ Outputs: map[string]string{
"foo-with-hyphens": "bar-with-hyphens", "foo-with-hyphens": "bar-with-hyphens",
}, },
Success: true,
}, },
"id_with_underscores": { "id_with_underscores": {
Conclusion: stepStatusSuccess,
Outcome: stepStatusFailure,
Outputs: map[string]string{ Outputs: map[string]string{
"foo_with_underscores": "bar_with_underscores", "foo_with_underscores": "bar_with_underscores",
}, },
Success: true,
}, },
}, },
} }
@ -107,8 +110,14 @@ func TestEvaluate(t *testing.T) {
{"github.run_id", "1", ""}, {"github.run_id", "1", ""},
{"github.run_number", "1", ""}, {"github.run_number", "1", ""},
{"job.status", "success", ""}, {"job.status", "success", ""},
{"steps.idwithnothing.conclusion", "success", ""},
{"steps.idwithnothing.outcome", "failure", ""},
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""}, {"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
{"steps.id-with-hyphens.conclusion", "success", ""},
{"steps.id-with-hyphens.outcome", "failure", ""},
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""}, {"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
{"steps.id_with_underscores.conclusion", "success", ""},
{"steps.id_with_underscores.outcome", "failure", ""},
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""}, {"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
{"runner.os", "Linux", ""}, {"runner.os", "Linux", ""},
{"matrix.os", "Linux", ""}, {"matrix.os", "Linux", ""},

View File

@ -52,9 +52,44 @@ func (rc *RunContext) String() string {
return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
} }
type stepStatus int
const (
stepStatusSuccess stepStatus = iota
stepStatusFailure
)
var stepStatusStrings = [...]string{
"success",
"failure",
}
func (s stepStatus) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s *stepStatus) UnmarshalText(b []byte) error {
str := string(b)
for i, name := range stepStatusStrings {
if name == str {
*s = stepStatus(i)
return nil
}
}
return fmt.Errorf("invalid step status %q", str)
}
func (s stepStatus) String() string {
if int(s) >= len(stepStatusStrings) {
return ""
}
return stepStatusStrings[s]
}
type stepResult struct { type stepResult struct {
Success bool `json:"success"`
Outputs map[string]string `json:"outputs"` Outputs map[string]string `json:"outputs"`
Conclusion stepStatus `json:"conclusion"`
Outcome stepStatus `json:"outcome"`
} }
// GetEnv returns the env for the context // GetEnv returns the env for the context
@ -266,7 +301,8 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
rc.CurrentStep = sc.Step.ID rc.CurrentStep = sc.Step.ID
rc.StepResults[rc.CurrentStep] = &stepResult{ rc.StepResults[rc.CurrentStep] = &stepResult{
Success: true, Outcome: stepStatusSuccess,
Conclusion: stepStatusSuccess,
Outputs: make(map[string]string), Outputs: make(map[string]string),
} }
runStep, err := rc.EvalBool(sc.Step.If.Value) runStep, err := rc.EvalBool(sc.Step.If.Value)
@ -278,7 +314,8 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
return err return err
} }
rc.ExprEval = exprEval rc.ExprEval = exprEval
rc.StepResults[rc.CurrentStep].Success = false rc.StepResults[rc.CurrentStep].Conclusion = stepStatusFailure
rc.StepResults[rc.CurrentStep].Outcome = stepStatusFailure
return err return err
} }
@ -300,12 +337,13 @@ func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
} else { } else {
common.Logger(ctx).Errorf(" \u274C Failure - %s", sc.Step) common.Logger(ctx).Errorf(" \u274C Failure - %s", sc.Step)
rc.StepResults[rc.CurrentStep].Outcome = stepStatusFailure
if sc.Step.ContinueOnError { if sc.Step.ContinueOnError {
common.Logger(ctx).Infof("Failed but continue next step") common.Logger(ctx).Infof("Failed but continue next step")
err = nil err = nil
rc.StepResults[rc.CurrentStep].Success = true rc.StepResults[rc.CurrentStep].Conclusion = stepStatusSuccess
} else { } else {
rc.StepResults[rc.CurrentStep].Success = false rc.StepResults[rc.CurrentStep].Conclusion = stepStatusFailure
} }
} }
return err return err
@ -500,7 +538,7 @@ type jobContext struct {
func (rc *RunContext) getJobContext() *jobContext { func (rc *RunContext) getJobContext() *jobContext {
jobStatus := "success" jobStatus := "success"
for _, stepStatus := range rc.StepResults { for _, stepStatus := range rc.StepResults {
if !stepStatus.Success { if stepStatus.Conclusion == stepStatusFailure {
jobStatus = "failure" jobStatus = "failure"
break break
} }

View File

@ -54,10 +54,11 @@ func TestRunContext_EvalBool(t *testing.T) {
}, },
StepResults: map[string]*stepResult{ StepResults: map[string]*stepResult{
"id1": { "id1": {
Conclusion: stepStatusSuccess,
Outcome: stepStatusFailure,
Outputs: map[string]string{ Outputs: map[string]string{
"foo": "bar", "foo": "bar",
}, },
Success: true,
}, },
}, },
} }
@ -73,6 +74,10 @@ func TestRunContext_EvalBool(t *testing.T) {
{in: "success()", out: true}, {in: "success()", out: true},
{in: "cancelled()", out: false}, {in: "cancelled()", out: false},
{in: "always()", out: true}, {in: "always()", out: true},
{in: "steps.id1.conclusion == 'success'", out: true},
{in: "steps.id1.conclusion != 'success'", out: false},
{in: "steps.id1.outcome == 'failure'", out: true},
{in: "steps.id1.outcome != 'failure'", out: false},
{in: "true", out: true}, {in: "true", out: true},
{in: "false", out: false}, {in: "false", out: false},
{in: "!true", wantErr: true}, {in: "!true", wantErr: true},

View File

@ -121,6 +121,8 @@ func TestRunEvent(t *testing.T) {
{"testdata", "issue-598", "push", "", platforms, ""}, {"testdata", "issue-598", "push", "", platforms, ""},
{"testdata", "env-and-path", "push", "", platforms, ""}, {"testdata", "env-and-path", "push", "", platforms, ""},
{"testdata", "outputs", "push", "", platforms, ""}, {"testdata", "outputs", "push", "", platforms, ""},
{"testdata", "steps-context/conclusion", "push", "", platforms, ""},
{"testdata", "steps-context/outcome", "push", "", platforms, ""},
{"../model/testdata", "strategy", "push", "", platforms, ""}, // TODO: move all testdata into pkg so we can validate it with planner and runner {"../model/testdata", "strategy", "push", "", platforms, ""}, // TODO: move all testdata into pkg so we can validate it with planner and runner
// {"testdata", "issue-228", "push", "", platforms, ""}, // TODO [igni]: Remove this once everything passes // {"testdata", "issue-228", "push", "", platforms, ""}, // TODO [igni]: Remove this once everything passes

View File

@ -640,7 +640,7 @@ func (sc *StepContext) execAsComposite(ctx context.Context, step *model.Step, _
// Setup the outputs for the composite steps // Setup the outputs for the composite steps
if _, ok := rcClone.StepResults[stepClone.ID]; !ok { if _, ok := rcClone.StepResults[stepClone.ID]; !ok {
rcClone.StepResults[stepClone.ID] = &stepResult{ rcClone.StepResults[stepClone.ID] = &stepResult{
Success: true, Conclusion: stepStatusSuccess,
Outputs: make(map[string]string), Outputs: make(map[string]string),
} }
} }

View File

@ -0,0 +1,14 @@
name: conclusion
on: push
jobs:
check:
runs-on: ubuntu-latest
steps:
- id: first
run: exit 0
- id: second
continue-on-error: true
run: exit 1
- run: echo '${{ steps.first.conclusion }}' | grep 'success'
- run: echo '${{ steps.second.conclusion }}' | grep 'success'

View File

@ -0,0 +1,14 @@
name: outcome
on: push
jobs:
check:
runs-on: ubuntu-latest
steps:
- id: first
run: exit 0
- id: second
continue-on-error: true
run: exit 1
- run: echo '${{ steps.first.outcome }}' | grep 'success'
- run: echo '${{ steps.second.outcome }}' | grep 'failure'