Refactor expression evaluator to use parser from actionlint package (#908)
* feat: implement expression evaluator Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * feat: integrate exprparser into act Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * Escape { and }, do not fail on missing properties * Fix empty inputs context * fix: contains() comparison for complex values Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
@@ -1,460 +1,33 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/nektos/act/pkg/exprparser"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var expressionPattern, operatorPattern *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
operatorPattern = regexp.MustCompile("^[!=><|&]+$")
|
||||
// ExpressionEvaluator is the interface for evaluating expressions
|
||||
type ExpressionEvaluator interface {
|
||||
evaluate(string, bool) (interface{}, error)
|
||||
Interpolate(string) string
|
||||
}
|
||||
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||
vm := rc.newVM()
|
||||
|
||||
return &expressionEvaluator{
|
||||
vm,
|
||||
}
|
||||
}
|
||||
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||
vm := sc.RunContext.newVM()
|
||||
configers := []func(*otto.Otto){
|
||||
sc.vmEnv(),
|
||||
sc.vmNeeds(),
|
||||
sc.vmSuccess(),
|
||||
sc.vmFailure(),
|
||||
}
|
||||
for _, configer := range configers {
|
||||
configer(vm)
|
||||
// todo: cleanup EvaluationEnvironment creation
|
||||
job := rc.Run.Job()
|
||||
strategy := make(map[string]interface{})
|
||||
if job.Strategy != nil {
|
||||
strategy["fail-fast"] = job.Strategy.FailFast
|
||||
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||
}
|
||||
|
||||
return &expressionEvaluator{
|
||||
vm,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpressionEvaluator is the interface for evaluating expressions
|
||||
type ExpressionEvaluator interface {
|
||||
Evaluate(string) (string, bool, error)
|
||||
Interpolate(string) string
|
||||
InterpolateWithStringCheck(string) (string, bool)
|
||||
Rewrite(string) string
|
||||
}
|
||||
|
||||
type expressionEvaluator struct {
|
||||
vm *otto.Otto
|
||||
}
|
||||
|
||||
func (ee *expressionEvaluator) Evaluate(in string) (string, bool, error) {
|
||||
if strings.HasPrefix(in, `secrets.`) {
|
||||
in = `secrets.` + strings.ToUpper(strings.SplitN(in, `.`, 2)[1])
|
||||
}
|
||||
re := ee.Rewrite(in)
|
||||
if re != in {
|
||||
log.Debugf("Evaluating '%s' instead of '%s'", re, in)
|
||||
}
|
||||
|
||||
val, err := ee.vm.Run(re)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if val.IsNull() || val.IsUndefined() {
|
||||
return "", false, nil
|
||||
}
|
||||
valAsString, err := val.ToString()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return valAsString, val.IsString(), err
|
||||
}
|
||||
|
||||
func (ee *expressionEvaluator) Interpolate(in string) string {
|
||||
interpolated, _ := ee.InterpolateWithStringCheck(in)
|
||||
return interpolated
|
||||
}
|
||||
|
||||
func (ee *expressionEvaluator) InterpolateWithStringCheck(in string) (string, bool) {
|
||||
errList := make([]error, 0)
|
||||
|
||||
out := in
|
||||
isString := false
|
||||
for {
|
||||
out = expressionPattern.ReplaceAllStringFunc(in, func(match string) string {
|
||||
// Extract and trim the actual expression inside ${{...}} delimiters
|
||||
expression := expressionPattern.ReplaceAllString(match, "$1")
|
||||
|
||||
// Evaluate the expression and retrieve errors if any
|
||||
evaluated, evaluatedIsString, err := ee.Evaluate(expression)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
}
|
||||
isString = evaluatedIsString
|
||||
return evaluated
|
||||
})
|
||||
if len(errList) > 0 {
|
||||
log.Errorf("Unable to interpolate string '%s' - %v", in, errList)
|
||||
break
|
||||
}
|
||||
if out == in {
|
||||
// No replacement occurred, we're done!
|
||||
break
|
||||
}
|
||||
in = out
|
||||
}
|
||||
return out, isString
|
||||
}
|
||||
|
||||
// Rewrite tries to transform any javascript property accessor into its bracket notation.
|
||||
// For instance, "object.property" would become "object['property']".
|
||||
func (ee *expressionEvaluator) Rewrite(in string) string {
|
||||
var buf strings.Builder
|
||||
r := strings.NewReader(in)
|
||||
for {
|
||||
c, _, err := r.ReadRune()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
//nolint
|
||||
switch {
|
||||
default:
|
||||
buf.WriteRune(c)
|
||||
case c == '\'':
|
||||
buf.WriteRune(c)
|
||||
ee.advString(&buf, r)
|
||||
case c == '.':
|
||||
buf.WriteString("['")
|
||||
ee.advPropertyName(&buf, r)
|
||||
buf.WriteString("']")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (*expressionEvaluator) advString(w *strings.Builder, r *strings.Reader) error {
|
||||
for {
|
||||
c, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c != '\'' {
|
||||
w.WriteRune(c)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handles a escaped string: ex. 'It''s ok'
|
||||
c, _, err = r.ReadRune()
|
||||
if err != nil {
|
||||
w.WriteString("'")
|
||||
return err
|
||||
}
|
||||
if c != '\'' {
|
||||
w.WriteString("'")
|
||||
if err := r.UnreadRune(); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
w.WriteString(`\'`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*expressionEvaluator) advPropertyName(w *strings.Builder, r *strings.Reader) error {
|
||||
for {
|
||||
c, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isLetter(c) {
|
||||
if err := r.UnreadRune(); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
w.WriteRune(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLetter(c rune) bool {
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z':
|
||||
return true
|
||||
case c >= 'A' && c <= 'Z':
|
||||
return true
|
||||
case c >= '0' && c <= '9':
|
||||
return true
|
||||
case c == '_' || c == '-':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) newVM() *otto.Otto {
|
||||
configers := []func(*otto.Otto){
|
||||
vmContains,
|
||||
vmStartsWith,
|
||||
vmEndsWith,
|
||||
vmFormat,
|
||||
vmJoin,
|
||||
vmToJSON,
|
||||
vmFromJSON,
|
||||
vmAlways,
|
||||
rc.vmCancelled(),
|
||||
rc.vmSuccess(),
|
||||
rc.vmFailure(),
|
||||
rc.vmHashFiles(),
|
||||
|
||||
rc.vmGithub(),
|
||||
rc.vmJob(),
|
||||
rc.vmSteps(),
|
||||
rc.vmRunner(),
|
||||
|
||||
rc.vmSecrets(),
|
||||
rc.vmStrategy(),
|
||||
rc.vmMatrix(),
|
||||
rc.vmEnv(),
|
||||
rc.vmNeeds(),
|
||||
rc.vmInputs(),
|
||||
}
|
||||
vm := otto.New()
|
||||
for _, configer := range configers {
|
||||
configer(vm)
|
||||
}
|
||||
return vm
|
||||
}
|
||||
|
||||
func vmContains(vm *otto.Otto) {
|
||||
_ = vm.Set("contains", func(searchString interface{}, searchValue string) bool {
|
||||
if searchStringString, ok := searchString.(string); ok {
|
||||
return strings.Contains(strings.ToLower(searchStringString), strings.ToLower(searchValue))
|
||||
} else if searchStringArray, ok := searchString.([]string); ok {
|
||||
for _, s := range searchStringArray {
|
||||
if strings.EqualFold(s, searchValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func vmStartsWith(vm *otto.Otto) {
|
||||
_ = vm.Set("startsWith", func(searchString string, searchValue string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
||||
})
|
||||
}
|
||||
|
||||
func vmEndsWith(vm *otto.Otto) {
|
||||
_ = vm.Set("endsWith", func(searchString string, searchValue string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
||||
})
|
||||
}
|
||||
|
||||
func vmFormat(vm *otto.Otto) {
|
||||
_ = vm.Set("format", func(s string, vals ...otto.Value) string {
|
||||
ex := regexp.MustCompile(`(\{[0-9]+\}|\{.?|\}.?)`)
|
||||
return ex.ReplaceAllStringFunc(s, func(seg string) string {
|
||||
switch seg {
|
||||
case "{{":
|
||||
return "{"
|
||||
case "}}":
|
||||
return "}"
|
||||
default:
|
||||
if len(seg) < 3 || !strings.HasPrefix(seg, "{") {
|
||||
log.Errorf("The following format string is invalid: '%v'", s)
|
||||
return ""
|
||||
}
|
||||
_i := seg[1 : len(seg)-1]
|
||||
i, err := strconv.ParseInt(_i, 10, 32)
|
||||
if err != nil {
|
||||
log.Errorf("The following format string is invalid: '%v'. Error: %v", s, err)
|
||||
return ""
|
||||
}
|
||||
if i >= int64(len(vals)) {
|
||||
log.Errorf("The following format string references more arguments than were supplied: '%v'", s)
|
||||
return ""
|
||||
}
|
||||
if vals[i].IsNull() || vals[i].IsUndefined() {
|
||||
return ""
|
||||
}
|
||||
return vals[i].String()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func vmJoin(vm *otto.Otto) {
|
||||
_ = vm.Set("join", func(element interface{}, optionalElem string) string {
|
||||
slist := make([]string, 0)
|
||||
if elementString, ok := element.(string); ok {
|
||||
slist = append(slist, elementString)
|
||||
} else if elementArray, ok := element.([]string); ok {
|
||||
slist = append(slist, elementArray...)
|
||||
}
|
||||
if optionalElem != "" {
|
||||
slist = append(slist, optionalElem)
|
||||
}
|
||||
return strings.Join(slist, " ")
|
||||
})
|
||||
}
|
||||
|
||||
func vmToJSON(vm *otto.Otto) {
|
||||
toJSON := func(o interface{}) string {
|
||||
rtn, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
log.Errorf("Unable to marshal: %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(rtn)
|
||||
}
|
||||
_ = vm.Set("toJSON", toJSON)
|
||||
_ = vm.Set("toJson", toJSON)
|
||||
}
|
||||
|
||||
func vmFromJSON(vm *otto.Otto) {
|
||||
fromJSON := func(str string) interface{} {
|
||||
var dat interface{}
|
||||
err := json.Unmarshal([]byte(str), &dat)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to unmarshal: %v", err)
|
||||
return dat
|
||||
}
|
||||
return dat
|
||||
}
|
||||
_ = vm.Set("fromJSON", fromJSON)
|
||||
_ = vm.Set("fromJson", fromJSON)
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmHashFiles() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("hashFiles", func(paths ...string) string {
|
||||
var files []string
|
||||
for i := range paths {
|
||||
newFiles, err := filepath.Glob(filepath.Join(rc.Config.Workdir, paths[i]))
|
||||
if err != nil {
|
||||
log.Errorf("Unable to glob.Glob: %v", err)
|
||||
return ""
|
||||
}
|
||||
files = append(files, newFiles...)
|
||||
}
|
||||
hasher := sha256.New()
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to os.Open: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(hasher, f); err != nil {
|
||||
log.Errorf("Unable to io.Copy: %v", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
log.Errorf("Unable to Close file: %v", err)
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmSuccess() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("success", func() bool {
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
|
||||
|
||||
for _, needs := range jobNeeds {
|
||||
if jobs[needs].Result != "success" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmFailure() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("failure", func() bool {
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
|
||||
|
||||
for _, needs := range jobNeeds {
|
||||
if jobs[needs].Result == "failure" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func vmAlways(vm *otto.Otto) {
|
||||
_ = vm.Set("always", func() bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
func (rc *RunContext) vmCancelled() func(vm *otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("cancelled", func() bool {
|
||||
return rc.getJobContext().Status == "cancelled"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmGithub() func(*otto.Otto) {
|
||||
github := rc.getGithubContext()
|
||||
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("github", github)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmEnv() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
env := rc.GetEnv()
|
||||
log.Debugf("context env => %v", env)
|
||||
_ = vm.Set("env", env)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) vmEnv() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
log.Debugf("context env => %v", sc.Env)
|
||||
_ = vm.Set("env", sc.Env)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmInputs() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("inputs", rc.Inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) vmNeeds() func(*otto.Otto) {
|
||||
jobs := sc.RunContext.Run.Workflow.Jobs
|
||||
jobNeeds := sc.RunContext.Run.Job().Needs()
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.Run.Job().Needs()
|
||||
|
||||
using := make(map[string]map[string]map[string]string)
|
||||
for _, needs := range jobNeeds {
|
||||
@@ -463,182 +36,225 @@ func (sc *StepContext) vmNeeds() func(*otto.Otto) {
|
||||
}
|
||||
}
|
||||
|
||||
return func(vm *otto.Otto) {
|
||||
log.Debugf("context needs => %v", using)
|
||||
_ = vm.Set("needs", using)
|
||||
secrets := rc.Config.Secrets
|
||||
if rc.Composite != nil {
|
||||
secrets = nil
|
||||
}
|
||||
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: rc.getGithubContext(),
|
||||
Env: rc.GetEnv(),
|
||||
Job: rc.getJobContext(),
|
||||
// todo: should be unavailable
|
||||
// but required to interpolate/evaluate the step outputs on the job
|
||||
Steps: rc.getStepsContext(),
|
||||
Runner: map[string]interface{}{
|
||||
"os": "Linux",
|
||||
"temp": "/tmp",
|
||||
"tool_cache": "/opt/hostedtoolcache",
|
||||
},
|
||||
Secrets: secrets,
|
||||
Strategy: strategy,
|
||||
Matrix: rc.Matrix,
|
||||
Needs: using,
|
||||
Inputs: rc.Inputs,
|
||||
}
|
||||
return expressionEvaluator{
|
||||
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||
Run: rc.Run,
|
||||
WorkingDir: rc.Config.Workdir,
|
||||
Context: "job",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) vmSuccess() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("success", func() bool {
|
||||
return sc.RunContext.getJobContext().Status == "success"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) vmFailure() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("failure", func() bool {
|
||||
return sc.RunContext.getJobContext().Status == "failure"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type vmNeedsStruct struct {
|
||||
Outputs map[string]string `json:"outputs"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmNeeds() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
needsFunc := func() otto.Value {
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.Run.Job().Needs()
|
||||
|
||||
using := make(map[string]vmNeedsStruct)
|
||||
for _, needs := range jobNeeds {
|
||||
using[needs] = vmNeedsStruct{
|
||||
Outputs: jobs[needs].Outputs,
|
||||
Result: jobs[needs].Result,
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("context needs => %+v", using)
|
||||
|
||||
value, err := vm.ToValue(using)
|
||||
if err != nil {
|
||||
return vm.MakeTypeError(err.Error())
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Results might change after the Otto VM was created
|
||||
// and initialized. To access the current state
|
||||
// we can't just pass a copy to Otto - instead we
|
||||
// created a 'live-binding'.
|
||||
// Technical Note: We don't want to pollute the global
|
||||
// js namespace (and add things github actions hasn't)
|
||||
// we delete the helper function after installing it
|
||||
// as a getter.
|
||||
global, _ := vm.Run("this")
|
||||
_ = global.Object().Set("__needs__", needsFunc)
|
||||
_, _ = vm.Run(`
|
||||
(function (global) {
|
||||
Object.defineProperty(global, 'needs', { get: global.__needs__ });
|
||||
delete global.__needs__;
|
||||
})(this)
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmJob() func(*otto.Otto) {
|
||||
job := rc.getJobContext()
|
||||
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("job", job)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmSteps() func(*otto.Otto) {
|
||||
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) {
|
||||
log.Debugf("context steps => %v", steps)
|
||||
_ = vm.Set("steps", steps)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmRunner() func(*otto.Otto) {
|
||||
runner := map[string]interface{}{
|
||||
"os": "Linux",
|
||||
"temp": "/tmp",
|
||||
"tool_cache": "/opt/hostedtoolcache",
|
||||
}
|
||||
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("runner", runner)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmSecrets() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
// Hide secrets from composite actions
|
||||
if rc.Composite == nil {
|
||||
_ = vm.Set("secrets", rc.Config.Secrets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmStrategy() func(*otto.Otto) {
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||
rc := sc.RunContext
|
||||
// todo: cleanup EvaluationEnvironment creation
|
||||
job := rc.Run.Job()
|
||||
strategy := make(map[string]interface{})
|
||||
if job.Strategy != nil {
|
||||
strategy["fail-fast"] = job.Strategy.FailFast
|
||||
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||
}
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("strategy", strategy)
|
||||
|
||||
jobs := rc.Run.Workflow.Jobs
|
||||
jobNeeds := rc.Run.Job().Needs()
|
||||
|
||||
using := make(map[string]map[string]map[string]string)
|
||||
for _, needs := range jobNeeds {
|
||||
using[needs] = map[string]map[string]string{
|
||||
"outputs": jobs[needs].Outputs,
|
||||
}
|
||||
}
|
||||
|
||||
secrets := rc.Config.Secrets
|
||||
if rc.Composite != nil {
|
||||
secrets = nil
|
||||
}
|
||||
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: rc.getGithubContext(),
|
||||
Env: rc.GetEnv(),
|
||||
Job: rc.getJobContext(),
|
||||
Steps: rc.getStepsContext(),
|
||||
Runner: map[string]interface{}{
|
||||
"os": "Linux",
|
||||
"temp": "/tmp",
|
||||
"tool_cache": "/opt/hostedtoolcache",
|
||||
},
|
||||
Secrets: secrets,
|
||||
Strategy: strategy,
|
||||
Matrix: rc.Matrix,
|
||||
Needs: using,
|
||||
// todo: should be unavailable
|
||||
// but required to interpolate/evaluate the inputs in actions/composite
|
||||
Inputs: rc.Inputs,
|
||||
}
|
||||
return expressionEvaluator{
|
||||
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||
Run: rc.Run,
|
||||
WorkingDir: rc.Config.Workdir,
|
||||
Context: "step",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) vmMatrix() func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
_ = vm.Set("matrix", rc.Matrix)
|
||||
type expressionEvaluator struct {
|
||||
interpreter exprparser.Interpreter
|
||||
}
|
||||
|
||||
func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) {
|
||||
evaluated, err := ee.interpreter.Evaluate(in, isIfExpression)
|
||||
return evaluated, err
|
||||
}
|
||||
|
||||
func (ee expressionEvaluator) Interpolate(in string) string {
|
||||
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||
return in
|
||||
}
|
||||
|
||||
expr, _ := rewriteSubExpression(in, true)
|
||||
if in != expr {
|
||||
log.Debugf("expression '%s' rewritten to '%s'", in, expr)
|
||||
}
|
||||
|
||||
evaluated, err := ee.evaluate(expr, false)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Debugf("expression '%s' evaluated to '%s'", expr, evaluated)
|
||||
|
||||
value, ok := evaluated.(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// EvalBool evaluates an expression against given evaluator
|
||||
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
|
||||
if splitPattern == nil {
|
||||
splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
|
||||
nextExpr, _ := rewriteSubExpression(expr, false)
|
||||
if expr != nextExpr {
|
||||
log.Debugf("expression '%s' rewritten to '%s'", expr, nextExpr)
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(expr), "!") {
|
||||
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
|
||||
|
||||
evaluated, err := evaluator.evaluate(nextExpr, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
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 := evaluator.InterpolateWithStringCheck(part)
|
||||
var result bool
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
joined := strings.Join(evaluatedParts, " ")
|
||||
v, _, err := evaluator.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
|
||||
default:
|
||||
return false, fmt.Errorf("Unable to map return type to boolean for '%s'", expr)
|
||||
}
|
||||
return true, nil
|
||||
|
||||
log.Debugf("expression '%s' evaluated to '%t'", nextExpr, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func escapeFormatString(in string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
|
||||
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
strPattern := regexp.MustCompile("(?:''|[^'])*'")
|
||||
pos := 0
|
||||
exprStart := -1
|
||||
strStart := -1
|
||||
var results []string
|
||||
formatOut := ""
|
||||
for pos < len(in) {
|
||||
if strStart > -1 {
|
||||
matches := strPattern.FindStringIndex(in[pos:])
|
||||
if matches == nil {
|
||||
panic("unclosed string.")
|
||||
}
|
||||
|
||||
strStart = -1
|
||||
pos += matches[1]
|
||||
} else if exprStart > -1 {
|
||||
exprEnd := strings.Index(in[pos:], "}}")
|
||||
strStart = strings.Index(in[pos:], "'")
|
||||
|
||||
if exprEnd > -1 && strStart > -1 {
|
||||
if exprEnd < strStart {
|
||||
strStart = -1
|
||||
} else {
|
||||
exprEnd = -1
|
||||
}
|
||||
}
|
||||
|
||||
if exprEnd > -1 {
|
||||
formatOut += fmt.Sprintf("{%d}", len(results))
|
||||
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
|
||||
pos += exprEnd + 2
|
||||
exprStart = -1
|
||||
} else if strStart > -1 {
|
||||
pos += strStart + 1
|
||||
} else {
|
||||
panic("unclosed expression.")
|
||||
}
|
||||
} else {
|
||||
exprStart = strings.Index(in[pos:], "${{")
|
||||
if exprStart != -1 {
|
||||
formatOut += escapeFormatString(in[pos : pos+exprStart])
|
||||
exprStart = pos + exprStart + 3
|
||||
pos = exprStart
|
||||
} else {
|
||||
formatOut += escapeFormatString(in[pos:])
|
||||
pos = len(in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", ")), nil
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
func createRunContext(t *testing.T) *RunContext {
|
||||
var yml yaml.Node
|
||||
err := yml.Encode(map[string][]interface{}{
|
||||
"os": {"Linux", "Windows"},
|
||||
@@ -20,7 +20,7 @@ func TestEvaluate(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
rc := &RunContext{
|
||||
return &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
Secrets: map[string]string{
|
||||
@@ -71,54 +71,50 @@ func TestEvaluate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateRunContext(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
out string
|
||||
out interface{}
|
||||
errMesg string
|
||||
}{
|
||||
{" 1 ", "1", ""},
|
||||
{"1 + 3", "4", ""},
|
||||
{"(1 + 3) * -2", "-8", ""},
|
||||
{" 1 ", 1, ""},
|
||||
// {"1 + 3", "4", ""},
|
||||
// {"(1 + 3) * -2", "-8", ""},
|
||||
{"'my text'", "my text", ""},
|
||||
{"contains('my text', 'te')", "true", ""},
|
||||
{"contains('my TEXT', 'te')", "true", ""},
|
||||
{"contains(['my text'], 'te')", "false", ""},
|
||||
{"contains(['foo','bar'], 'bar')", "true", ""},
|
||||
{"startsWith('hello world', 'He')", "true", ""},
|
||||
{"endsWith('hello world', 'ld')", "true", ""},
|
||||
{"contains('my text', 'te')", true, ""},
|
||||
{"contains('my TEXT', 'te')", true, ""},
|
||||
{"contains(fromJSON('[\"my text\"]'), 'te')", false, ""},
|
||||
{"contains(fromJSON('[\"foo\",\"bar\"]'), 'bar')", true, ""},
|
||||
{"startsWith('hello world', 'He')", true, ""},
|
||||
{"endsWith('hello world', 'ld')", true, ""},
|
||||
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
|
||||
{"join(['hello'],'octocat')", "hello octocat", ""},
|
||||
{"join(['hello','mona','the'],'octocat')", "hello mona the octocat", ""},
|
||||
{"join('hello','mona')", "hello mona", ""},
|
||||
{"toJSON({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
||||
{"toJson({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
||||
{"join(fromJSON('[\"hello\"]'),'octocat')", "hello", ""},
|
||||
{"join(fromJSON('[\"hello\",\"mona\",\"the\"]'),'octocat')", "hellooctocatmonaoctocatthe", ""},
|
||||
{"join('hello','mona')", "hello", ""},
|
||||
{"toJSON(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||
{"toJson(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
|
||||
{"hashFiles('**/non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non.extant.files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non''extant''files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"success()", "true", ""},
|
||||
{"failure()", "false", ""},
|
||||
{"always()", "true", ""},
|
||||
{"cancelled()", "false", ""},
|
||||
// github does return an empty string for non-existent files
|
||||
{"hashFiles('**/non-extant-files')", "", ""},
|
||||
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "", ""},
|
||||
{"hashFiles('**/non.extant.files')", "", ""},
|
||||
{"hashFiles('**/non''extant''files')", "", ""},
|
||||
{"success()", true, ""},
|
||||
{"failure()", false, ""},
|
||||
{"always()", true, ""},
|
||||
{"cancelled()", false, ""},
|
||||
{"github.workflow", "test-workflow", ""},
|
||||
{"github.actor", "nektos/act", ""},
|
||||
{"github.run_id", "1", ""},
|
||||
{"github.run_number", "1", ""},
|
||||
{"job.status", "success", ""},
|
||||
{"steps.idwithnothing.conclusion", "success", ""},
|
||||
{"steps.idwithnothing.outcome", "failure", ""},
|
||||
{"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_underscores.conclusion", "success", ""},
|
||||
{"steps.id_with_underscores.outcome", "failure", ""},
|
||||
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
||||
{"runner.os", "Linux", ""},
|
||||
{"matrix.os", "Linux", ""},
|
||||
{"matrix.foo", "bar", ""},
|
||||
@@ -139,7 +135,47 @@ func TestEvaluate(t *testing.T) {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, _, err := ee.Evaluate(table.in)
|
||||
out, err := ee.evaluate(table.in, false)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
} else {
|
||||
assertObject.Error(err, table.in)
|
||||
assertObject.Equal(table.errMesg, err.Error(), table.in)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateStepContext(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
|
||||
sc := &StepContext{
|
||||
RunContext: rc,
|
||||
}
|
||||
ee := sc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
out interface{}
|
||||
errMesg string
|
||||
}{
|
||||
{"steps.idwithnothing.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.idwithnothing.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
|
||||
{"steps.id-with-hyphens.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.id-with-hyphens.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
|
||||
{"steps.id_with_underscores.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.id_with_underscores.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := ee.evaluate(table.in, false)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
@@ -181,7 +217,12 @@ func TestInterpolate(t *testing.T) {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{" ${{1}} to ${{2}} ", " 1 to 2 "},
|
||||
{" text ", " text "},
|
||||
{" $text ", " $text "},
|
||||
{" ${text} ", " ${text} "},
|
||||
{" ${{ 1 }} to ${{2}} ", " 1 to 2 "},
|
||||
{" ${{ (true || false) }} to ${{2}} ", " true to 2 "},
|
||||
{" ${{ (false || '}}' ) }} to ${{2}} ", " }} to 2 "},
|
||||
{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
|
||||
{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
|
||||
{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
|
||||
@@ -205,12 +246,13 @@ func TestInterpolate(t *testing.T) {
|
||||
{"${{ env.SOMETHING_TRUE || false }}", "true"},
|
||||
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
||||
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
||||
{"${{ fromJSON('{}') < 2 }}", "false"},
|
||||
}
|
||||
|
||||
updateTestExpressionWorkflow(t, tables, rc)
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
t.Run("interpolate", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out := ee.Interpolate(table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
@@ -247,7 +289,7 @@ jobs:
|
||||
`, envs)
|
||||
// editorconfig-checker-enable
|
||||
for _, table := range tables {
|
||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
|
||||
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
||||
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
||||
@@ -268,43 +310,56 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
rc := &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "job1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"job1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
re string
|
||||
func TestRewriteSubExpression(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"ecole", "ecole"},
|
||||
{"ecole.centrale", "ecole['centrale']"},
|
||||
{"ecole['centrale']", "ecole['centrale']"},
|
||||
{"ecole.centrale.paris", "ecole['centrale']['paris']"},
|
||||
{"ecole['centrale'].paris", "ecole['centrale']['paris']"},
|
||||
{"ecole.centrale['paris']", "ecole['centrale']['paris']"},
|
||||
{"ecole['centrale']['paris']", "ecole['centrale']['paris']"},
|
||||
{"ecole.centrale-paris", "ecole['centrale-paris']"},
|
||||
{"ecole['centrale-paris']", "ecole['centrale-paris']"},
|
||||
{"ecole.centrale_paris", "ecole['centrale_paris']"},
|
||||
{"ecole['centrale_paris']", "ecole['centrale_paris']"},
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "${{ true }}"},
|
||||
{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
|
||||
{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
|
||||
{in: "${{ '}}' }}", out: "${{ '}}' }}"},
|
||||
{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
|
||||
{in: "${{ '''' }}", out: "${{ '''' }}"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
|
||||
{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
|
||||
{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpression", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
re := ee.Rewrite(table.in)
|
||||
assertObject.Equal(table.re, re, table.in)
|
||||
out, err := rewriteSubExpression(table.in, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "format('{0}', true)"},
|
||||
{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
}
|
||||
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := rewriteSubExpression(table.in, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -426,19 +426,6 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var splitPattern *regexp.Regexp
|
||||
|
||||
func previousOrNextPartIsAnOperator(i int, parts []string) bool {
|
||||
operator := false
|
||||
if i > 0 {
|
||||
operator = operatorPattern.MatchString(parts[i-1])
|
||||
}
|
||||
if i+1 < len(parts) {
|
||||
operator = operator || operatorPattern.MatchString(parts[i+1])
|
||||
}
|
||||
return operator
|
||||
}
|
||||
|
||||
func mergeMaps(maps ...map[string]string) map[string]string {
|
||||
rtnMap := make(map[string]string)
|
||||
for _, m := range maps {
|
||||
@@ -499,17 +486,6 @@ func (rc *RunContext) getStepsContext() map[string]*model.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
|
||||
}
|
||||
|
||||
func (rc *RunContext) getGithubContext() *model.GithubContext {
|
||||
ghc := &model.GithubContext{
|
||||
Event: make(map[string]interface{}),
|
||||
@@ -784,12 +760,11 @@ func (rc *RunContext) handleCredentials() (username, password string, err error)
|
||||
}
|
||||
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
var ok bool
|
||||
if username, ok = ee.InterpolateWithStringCheck(container.Credentials["username"]); !ok {
|
||||
if username = ee.Interpolate(container.Credentials["username"]); username == "" {
|
||||
err = fmt.Errorf("failed to interpolate container.credentials.username")
|
||||
return
|
||||
}
|
||||
if password, ok = ee.InterpolateWithStringCheck(container.Credentials["password"]); !ok {
|
||||
if password = ee.Interpolate(container.Credentials["password"]); password == "" {
|
||||
err = fmt.Errorf("failed to interpolate container.credentials.password")
|
||||
return
|
||||
}
|
||||
|
@@ -75,14 +75,16 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
{in: "success()", out: true},
|
||||
{in: "cancelled()", out: false},
|
||||
{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},
|
||||
// TODO: move to sc.NewExpressionEvaluator(), because "steps" context is not available here
|
||||
// {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: "false", out: false},
|
||||
{in: "!true", wantErr: true},
|
||||
{in: "!false", wantErr: true},
|
||||
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||
// {in: "!true", wantErr: true},
|
||||
// {in: "!false", wantErr: true},
|
||||
{in: "1 != 0", out: true},
|
||||
{in: "1 != 1", out: false},
|
||||
{in: "${{ 1 != 0 }}", out: true},
|
||||
@@ -100,14 +102,15 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
{in: "env.UNKNOWN == 'true'", out: false},
|
||||
{in: "env.UNKNOWN", out: false},
|
||||
// Inline expressions
|
||||
{in: "env.SOME_TEXT", out: true}, // this is because Boolean('text') is true in Javascript
|
||||
{in: "env.SOME_TEXT", out: true},
|
||||
{in: "env.SOME_TEXT == 'text'", out: true},
|
||||
{in: "env.SOMETHING_TRUE == 'true'", out: true},
|
||||
{in: "env.SOMETHING_FALSE == 'true'", out: false},
|
||||
{in: "env.SOMETHING_TRUE", out: true},
|
||||
{in: "env.SOMETHING_FALSE", out: true}, // this is because Boolean('text') is true in Javascript
|
||||
{in: "!env.SOMETHING_TRUE", wantErr: true},
|
||||
{in: "!env.SOMETHING_FALSE", wantErr: true},
|
||||
{in: "env.SOMETHING_FALSE", out: true},
|
||||
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||
// {in: "!env.SOMETHING_TRUE", wantErr: true},
|
||||
// {in: "!env.SOMETHING_FALSE", wantErr: true},
|
||||
{in: "${{ !env.SOMETHING_TRUE }}", out: false},
|
||||
{in: "${{ !env.SOMETHING_FALSE }}", out: false},
|
||||
{in: "${{ ! env.SOMETHING_TRUE }}", out: false},
|
||||
@@ -123,7 +126,8 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
{in: "${{ env.SOMETHING_TRUE && true }}", out: true},
|
||||
{in: "${{ env.SOMETHING_FALSE || true }}", out: true},
|
||||
{in: "${{ env.SOMETHING_FALSE || false }}", out: true},
|
||||
{in: "!env.SOMETHING_TRUE || true", wantErr: true},
|
||||
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||
// {in: "!env.SOMETHING_TRUE || true", wantErr: true},
|
||||
{in: "${{ env.SOMETHING_TRUE == 'true'}}", out: true},
|
||||
{in: "${{ env.SOMETHING_FALSE == 'true'}}", out: false},
|
||||
{in: "${{ env.SOMETHING_FALSE == 'false'}}", out: true},
|
||||
@@ -198,7 +202,7 @@ jobs:
|
||||
if table.wantErr || strings.HasPrefix(table.in, "github.actor") {
|
||||
continue
|
||||
}
|
||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
|
||||
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
||||
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
||||
|
Reference in New Issue
Block a user