Expression evaluator fixes (#1009)

* refactor: remove debug error output

Errors should always be logged with an error level and not debug level.
Since the error is returned here, it will be logged later as an error.
Presumably this was a leftover from debugging the executor chain in:
PR: #971

* refactor: debug log wich expression is going to be evaluated

* fix: handle nil in EvalBool

We've seen this issue when the env map is not set-up properly,
i.e. when the env map is nil, EvalBool might return nil, which should
be handled as a falsy value.

* fix: fail on error in if expression and return the evaluation error

Stop running the workflow in case an expression cannot be evaluated.

Fixes: #1008

* fix: remove quotes from inside expression syntax in test

It looks like having an expression inside double quotes inside the
expression syntax is not valid: https://github.com/ZauberNerd/act-test/actions/runs/1881986429
The workflow is not valid. .github/workflows/test.yml (Line: 10, Col: 13): Unexpected symbol: '"endsWith'. Located at position 1 within expression: "endsWith('Hello world', 'ld')"

* refactor: export IsTruthy function from exprparser package

* refactor: use IsTruthy function in EvalBool

* refactor: move debug log for expression rewrite to rewrite function

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Björn Brauer 2022-02-25 19:39:50 +01:00 committed by GitHub
parent 7d433968e5
commit c24cfc72f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 37 additions and 64 deletions

View file

@ -136,7 +136,6 @@ func (e Executor) Then(then Executor) Executor {
case Warning: case Warning:
log.Warning(err.Error()) log.Warning(err.Error())
default: default:
log.Debugf("%+v", err)
return err return err
} }
} }

View file

@ -274,7 +274,7 @@ func (impl *interperterImpl) evaluateNot(notNode *actionlint.NotOpNode) (interfa
return nil, err return nil, err
} }
return !impl.isTruthy(reflect.ValueOf(operand)), nil return !IsTruthy(operand), nil
} }
func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNode) (interface{}, error) { func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNode) (interface{}, error) {
@ -434,7 +434,8 @@ func (impl *interperterImpl) compareNumber(left float64, right float64, kind act
} }
} }
func (impl *interperterImpl) isTruthy(value reflect.Value) bool { func IsTruthy(input interface{}) bool {
value := reflect.ValueOf(input)
switch value.Kind() { switch value.Kind() {
case reflect.Bool: case reflect.Bool:
return value.Bool() return value.Bool()
@ -452,10 +453,7 @@ func (impl *interperterImpl) isTruthy(value reflect.Value) bool {
return value.Float() != 0 return value.Float() != 0
case reflect.Map: case reflect.Map, reflect.Slice:
return true
case reflect.Slice:
return true return true
default: default:
@ -503,14 +501,14 @@ func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.Logi
switch compareNode.Kind { switch compareNode.Kind {
case actionlint.LogicalOpNodeKindAnd: case actionlint.LogicalOpNodeKindAnd:
if impl.isTruthy(leftValue) { if IsTruthy(left) {
return impl.getSafeValue(rightValue), nil return impl.getSafeValue(rightValue), nil
} }
return impl.getSafeValue(leftValue), nil return impl.getSafeValue(leftValue), nil
case actionlint.LogicalOpNodeKindOr: case actionlint.LogicalOpNodeKindOr:
if impl.isTruthy(leftValue) { if IsTruthy(left) {
return impl.getSafeValue(leftValue), nil return impl.getSafeValue(leftValue), nil
} }

View file

@ -2,7 +2,6 @@ package runner
import ( import (
"fmt" "fmt"
"math"
"regexp" "regexp"
"strings" "strings"
@ -128,7 +127,9 @@ type expressionEvaluator struct {
} }
func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) { func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) {
log.Debugf("evaluating expression '%s'", in)
evaluated, err := ee.interpreter.Evaluate(in, isIfExpression) evaluated, err := ee.interpreter.Evaluate(in, isIfExpression)
log.Debugf("expression '%s' evaluated to '%t'", in, evaluated)
return evaluated, err return evaluated, err
} }
@ -141,9 +142,6 @@ func (ee expressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
return nil return nil
} }
expr, _ := rewriteSubExpression(in, false) expr, _ := rewriteSubExpression(in, false)
if in != expr {
log.Debugf("expression '%s' rewritten to '%s'", in, expr)
}
res, err := ee.evaluate(expr, false) res, err := ee.evaluate(expr, false)
if err != nil { if err != nil {
return err return err
@ -214,18 +212,12 @@ func (ee expressionEvaluator) Interpolate(in string) string {
} }
expr, _ := rewriteSubExpression(in, true) expr, _ := rewriteSubExpression(in, true)
if in != expr {
log.Debugf("expression '%s' rewritten to '%s'", in, expr)
}
evaluated, err := ee.evaluate(expr, false) evaluated, err := ee.evaluate(expr, false)
if err != nil { if err != nil {
log.Errorf("Unable to interpolate expression '%s': %s", expr, err) log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
return "" return ""
} }
log.Debugf("expression '%s' evaluated to '%s'", expr, evaluated)
value, ok := evaluated.(string) value, ok := evaluated.(string)
if !ok { if !ok {
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr)) panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
@ -237,37 +229,13 @@ func (ee expressionEvaluator) Interpolate(in string) string {
// EvalBool evaluates an expression against given evaluator // EvalBool evaluates an expression against given evaluator
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) { func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
nextExpr, _ := rewriteSubExpression(expr, false) nextExpr, _ := rewriteSubExpression(expr, false)
if expr != nextExpr {
log.Debugf("expression '%s' rewritten to '%s'", expr, nextExpr)
}
evaluated, err := evaluator.evaluate(nextExpr, true) evaluated, err := evaluator.evaluate(nextExpr, true)
if err != nil { if err != nil {
return false, err return false, err
} }
var result bool return exprparser.IsTruthy(evaluated), nil
switch t := evaluated.(type) {
case bool:
result = t
case string:
result = t != ""
case int:
result = t != 0
case float64:
if math.IsNaN(t) {
result = false
} else {
result = t != 0
}
default:
return false, fmt.Errorf("Unable to map return type to boolean for '%s'", expr)
}
log.Debugf("expression '%s' evaluated to '%t'", nextExpr, result)
return result, nil
} }
func escapeFormatString(in string) string { func escapeFormatString(in string) string {
@ -334,5 +302,9 @@ func rewriteSubExpression(in string, forceFormat bool) (string, error) {
return in, nil return in, nil
} }
return fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", ")), nil out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
if in != out {
log.Debugf("expression '%s' rewritten to '%s'", in, out)
}
return out, nil
} }

View file

@ -275,7 +275,18 @@ func (rc *RunContext) steps() []*model.Step {
// Executor returns a pipeline executor for all the steps in the job // Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) Executor() common.Executor { func (rc *RunContext) Executor() common.Executor {
return newJobExecutor(rc).If(rc.isEnabled) return func(ctx context.Context) error {
isEnabled, err := rc.isEnabled(ctx)
if err != nil {
return err
}
if isEnabled {
return newJobExecutor(rc)(ctx)
}
return nil
}
} }
// Executor returns a pipeline executor for all the steps in the job // Executor returns a pipeline executor for all the steps in the job
@ -405,17 +416,16 @@ func (rc *RunContext) hostname() string {
return *hostname return *hostname
} }
func (rc *RunContext) isEnabled(ctx context.Context) bool { func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
job := rc.Run.Job() job := rc.Run.Job()
l := common.Logger(ctx) l := common.Logger(ctx)
runJob, err := EvalBool(rc.ExprEval, job.If.Value) runJob, err := EvalBool(rc.ExprEval, job.If.Value)
if err != nil { if err != nil {
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", job.Name) return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, err)
return false
} }
if !runJob { if !runJob {
l.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value) l.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value)
return false return false, nil
} }
img := rc.platformImage() img := rc.platformImage()
@ -428,9 +438,9 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool {
platformName := rc.ExprEval.Interpolate(runnerLabel) platformName := rc.ExprEval.Interpolate(runnerLabel)
l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName) l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
} }
return false return false, nil
} }
return true return true, nil
} }
func mergeMaps(maps ...map[string]string) map[string]string { func mergeMaps(maps ...map[string]string) map[string]string {

View file

@ -171,13 +171,7 @@ func (sc *StepContext) interpolateEnv(exprEval ExpressionEvaluator) {
func (sc *StepContext) isEnabled(ctx context.Context) (bool, error) { func (sc *StepContext) isEnabled(ctx context.Context) (bool, error) {
runStep, err := EvalBool(sc.NewExpressionEvaluator(), sc.Step.If.Value) runStep, err := EvalBool(sc.NewExpressionEvaluator(), sc.Step.If.Value)
if err != nil { if err != nil {
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", sc.Step) return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", sc.Step.If.Value, err)
exprEval, err := sc.setupEnv(ctx)
if err != nil {
return false, err
}
sc.RunContext.ExprEval = exprEval
return false, err
} }
return runStep, nil return runStep, nil

View file

@ -1,10 +1,10 @@
name: issue-597 name: issue-597
on: push on: push
jobs: jobs:
my_first_job: my_first_job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: My first false step - name: My first false step
@ -14,7 +14,7 @@ jobs:
ref: refs/pull/${{github.event.pull_request.number}}/merge ref: refs/pull/${{github.event.pull_request.number}}/merge
fetch-depth: 5 fetch-depth: 5
- name: My first true step - name: My first true step
if: ${{"endsWith('Hello world', 'ld')"}} if: ${{endsWith('Hello world', 'ld')}}
uses: actions/hello-world-javascript-action@main uses: actions/hello-world-javascript-action@main
with: with:
who-to-greet: "Renst the Octocat" who-to-greet: "Renst the Octocat"

View file

@ -17,7 +17,7 @@ jobs:
secret_input: ${{secrets.test_input_optional || 'NO SUCH SECRET'}} secret_input: ${{secrets.test_input_optional || 'NO SUCH SECRET'}}
env: env:
secret_input: ${{secrets.test_input_optional || 'NO SUCH SECRET'}} secret_input: ${{secrets.test_input_optional || 'NO SUCH SECRET'}}
- if: steps.composite.outputs.test_output != "test_output_value" - if: steps.composite.outputs.test_output != 'test_output_value'
run: | run: |
echo "steps.composite.outputs.test_output=${{ steps.composite.outputs.test_output }}" echo "steps.composite.outputs.test_output=${{ steps.composite.outputs.test_output }}"
exit 1 exit 1
@ -38,7 +38,7 @@ jobs:
test_input_optional_with_default_overriden: 'test_input_optional_with_default_overriden' test_input_optional_with_default_overriden: 'test_input_optional_with_default_overriden'
test_input_required_with_default_overriden: 'test_input_required_with_default_overriden' test_input_required_with_default_overriden: 'test_input_required_with_default_overriden'
- if: steps.composite2.outputs.test_output != "test_output_value" - if: steps.composite2.outputs.test_output != 'test_output_value'
run: | run: |
echo "steps.composite.outputs.test_output=${{ steps.composite2.outputs.test_output }}" echo "steps.composite.outputs.test_output=${{ steps.composite2.outputs.test_output }}"
exit 1 exit 1