2020-02-13 01:27:37 -06:00
|
|
|
package runner
|
|
|
|
|
|
|
|
import (
|
2021-12-08 14:57:42 -06:00
|
|
|
"fmt"
|
2020-02-13 01:27:37 -06:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
"github.com/nektos/act/pkg/exprparser"
|
2020-03-06 15:39:01 -06:00
|
|
|
log "github.com/sirupsen/logrus"
|
2022-02-15 10:35:02 -06:00
|
|
|
"gopkg.in/yaml.v3"
|
2020-02-13 01:27:37 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// ExpressionEvaluator is the interface for evaluating expressions
|
|
|
|
type ExpressionEvaluator interface {
|
2022-01-21 10:07:20 -06:00
|
|
|
evaluate(string, bool) (interface{}, error)
|
2022-02-15 10:35:02 -06:00
|
|
|
EvaluateYamlNode(node *yaml.Node) error
|
2020-02-14 02:41:20 -06:00
|
|
|
Interpolate(string) string
|
2020-02-13 01:27:37 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
// NewExpressionEvaluator creates a new evaluator
|
|
|
|
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
|
|
|
// 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
|
2020-02-13 01:27:37 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
jobs := rc.Run.Workflow.Jobs
|
|
|
|
jobNeeds := rc.Run.Job().Needs()
|
2020-02-13 01:27:37 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
using := make(map[string]map[string]map[string]string)
|
|
|
|
for _, needs := range jobNeeds {
|
|
|
|
using[needs] = map[string]map[string]string{
|
|
|
|
"outputs": jobs[needs].Outputs,
|
2020-09-01 15:55:29 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
secrets := rc.Config.Secrets
|
|
|
|
if rc.Composite != nil {
|
|
|
|
secrets = nil
|
2020-02-13 01:27:37 -06:00
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
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,
|
2020-02-13 13:47:38 -06:00
|
|
|
}
|
2022-01-21 10:07:20 -06:00
|
|
|
return expressionEvaluator{
|
|
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
|
|
Run: rc.Run,
|
|
|
|
WorkingDir: rc.Config.Workdir,
|
|
|
|
Context: "job",
|
|
|
|
}),
|
2020-02-14 02:41:20 -06:00
|
|
|
}
|
2020-02-13 13:47:38 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
// 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
|
2020-02-13 13:47:38 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
jobs := rc.Run.Workflow.Jobs
|
|
|
|
jobNeeds := rc.Run.Job().Needs()
|
2021-07-01 10:20:20 -05:00
|
|
|
|
|
|
|
using := make(map[string]map[string]map[string]string)
|
|
|
|
for _, needs := range jobNeeds {
|
|
|
|
using[needs] = map[string]map[string]string{
|
|
|
|
"outputs": jobs[needs].Outputs,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
secrets := rc.Config.Secrets
|
|
|
|
if rc.Composite != nil {
|
|
|
|
secrets = nil
|
2021-07-01 10:20:20 -05:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
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,
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
2022-01-21 10:07:20 -06:00
|
|
|
return expressionEvaluator{
|
|
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
|
|
Run: rc.Run,
|
|
|
|
WorkingDir: rc.Config.Workdir,
|
|
|
|
Context: "step",
|
|
|
|
}),
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
type expressionEvaluator struct {
|
|
|
|
interpreter exprparser.Interpreter
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) {
|
2022-02-25 12:39:50 -06:00
|
|
|
log.Debugf("evaluating expression '%s'", in)
|
2022-01-21 10:07:20 -06:00
|
|
|
evaluated, err := ee.interpreter.Evaluate(in, isIfExpression)
|
2022-02-25 12:39:50 -06:00
|
|
|
log.Debugf("expression '%s' evaluated to '%t'", in, evaluated)
|
2022-01-21 10:07:20 -06:00
|
|
|
return evaluated, err
|
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
|
2022-02-15 10:35:02 -06:00
|
|
|
func (ee expressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
|
|
|
|
var in string
|
|
|
|
if err := node.Decode(&in); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
expr, _ := rewriteSubExpression(in, false)
|
|
|
|
res, err := ee.evaluate(expr, false)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return node.Encode(res)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ee expressionEvaluator) evaluateMappingYamlNode(node *yaml.Node) error {
|
|
|
|
// GitHub has this undocumented feature to merge maps, called insert directive
|
|
|
|
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
|
|
|
for i := 0; i < len(node.Content)/2; {
|
|
|
|
k := node.Content[i*2]
|
|
|
|
v := node.Content[i*2+1]
|
|
|
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var sk string
|
|
|
|
// Merge the nested map of the insert directive
|
|
|
|
if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
|
|
|
|
node.Content = append(append(node.Content[:i*2], v.Content...), node.Content[(i+1)*2:]...)
|
|
|
|
i += len(v.Content) / 2
|
|
|
|
} else {
|
|
|
|
if err := ee.EvaluateYamlNode(k); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ee expressionEvaluator) evaluateSequenceYamlNode(node *yaml.Node) error {
|
|
|
|
for i := 0; i < len(node.Content); {
|
|
|
|
v := node.Content[i]
|
|
|
|
// Preserve nested sequences
|
|
|
|
wasseq := v.Kind == yaml.SequenceNode
|
|
|
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// GitHub has this undocumented feature to merge sequences / arrays
|
|
|
|
// We have a nested sequence via evaluation, merge the arrays
|
|
|
|
if v.Kind == yaml.SequenceNode && !wasseq {
|
|
|
|
node.Content = append(append(node.Content[:i], v.Content...), node.Content[i+1:]...)
|
|
|
|
i += len(v.Content)
|
|
|
|
} else {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ee expressionEvaluator) EvaluateYamlNode(node *yaml.Node) error {
|
|
|
|
switch node.Kind {
|
|
|
|
case yaml.ScalarNode:
|
|
|
|
return ee.evaluateScalarYamlNode(node)
|
|
|
|
case yaml.MappingNode:
|
|
|
|
return ee.evaluateMappingYamlNode(node)
|
|
|
|
case yaml.SequenceNode:
|
|
|
|
return ee.evaluateSequenceYamlNode(node)
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
func (ee expressionEvaluator) Interpolate(in string) string {
|
|
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
|
|
return in
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
expr, _ := rewriteSubExpression(in, true)
|
|
|
|
evaluated, err := ee.evaluate(expr, false)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
|
|
|
return ""
|
2020-02-13 13:47:38 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
value, ok := evaluated.(string)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
|
2021-11-27 11:55:26 -06:00
|
|
|
}
|
2020-02-13 13:47:38 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
return value
|
2020-02-13 13:47:38 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
// EvalBool evaluates an expression against given evaluator
|
|
|
|
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
|
|
|
|
nextExpr, _ := rewriteSubExpression(expr, false)
|
2020-02-14 02:41:20 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
evaluated, err := evaluator.evaluate(nextExpr, true)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2022-02-25 12:39:50 -06:00
|
|
|
return exprparser.IsTruthy(evaluated), nil
|
2020-02-14 02:41:20 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
func escapeFormatString(in string) string {
|
|
|
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
2020-02-14 02:41:20 -06:00
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
//nolint:gocyclo
|
|
|
|
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
|
|
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
|
|
return in, nil
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
2022-01-21 10:07:20 -06:00
|
|
|
|
|
|
|
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.")
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
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
|
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
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)
|
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
2022-01-21 10:07:20 -06:00
|
|
|
}
|
2021-12-08 14:57:42 -06:00
|
|
|
|
2022-01-21 10:07:20 -06:00
|
|
|
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
|
|
|
return in, nil
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|
2022-01-21 10:07:20 -06:00
|
|
|
|
2022-02-25 12:39:50 -06:00
|
|
|
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
|
2021-12-08 14:57:42 -06:00
|
|
|
}
|