summaryrefslogtreecommitdiffstats
path: root/pkg/jobparser/evaluator.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/jobparser/evaluator.go')
-rw-r--r--pkg/jobparser/evaluator.go185
1 files changed, 185 insertions, 0 deletions
diff --git a/pkg/jobparser/evaluator.go b/pkg/jobparser/evaluator.go
new file mode 100644
index 0000000..80a1397
--- /dev/null
+++ b/pkg/jobparser/evaluator.go
@@ -0,0 +1,185 @@
+package jobparser
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/nektos/act/pkg/exprparser"
+ "gopkg.in/yaml.v3"
+)
+
+// ExpressionEvaluator is copied from runner.expressionEvaluator,
+// to avoid unnecessary dependencies
+type ExpressionEvaluator struct {
+ interpreter exprparser.Interpreter
+}
+
+func NewExpressionEvaluator(interpreter exprparser.Interpreter) *ExpressionEvaluator {
+ return &ExpressionEvaluator{interpreter: interpreter}
+}
+
+func (ee ExpressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
+ evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
+
+ return evaluated, err
+}
+
+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, exprparser.DefaultStatusCheckNone)
+ 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
+ }
+}
+
+func (ee ExpressionEvaluator) Interpolate(in string) string {
+ if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
+ return in
+ }
+
+ expr, _ := rewriteSubExpression(in, true)
+ evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
+ if err != nil {
+ return ""
+ }
+
+ value, ok := evaluated.(string)
+ if !ok {
+ panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
+ }
+
+ return value
+}
+
+func escapeFormatString(in string) string {
+ return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
+}
+
+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
+ }
+
+ out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
+ return out, nil
+}