186 lines
4.7 KiB
Go
186 lines
4.7 KiB
Go
|
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
|
||
|
}
|