679cac1677
* test: add test case for #1319 * fix: setup of composite inputs This change fixes the composite action setup handling of inputs. All inputs are taken from the env now. The env is composed of the 'level above'. For example: - step env -> taken from run context - action env -> taken from step env - composite env -> taken from action env Before this change the env setup for steps, actions and composite run contexts was harder to understand as all parts looked into one of these: parent run context, step, action, composite run context. Now the 'data flow' is from higher levels to lower levels which should make it more clean. Fixes #1319 * test: add simple remote composite action test Since we don't have a remote composite test at all before this, we need at least the simplest case. This does not check every feature, but ensures basic availability of remote composite actions. * refactor: move ActionRef and ActionRepository Moving ActionRef and ActionRepository from RunContext into the step, allows us to remove the - more or less - ugly copy operations from the RunContext. This is more clean, as each step does hold the data required anyway and the RunContext shouldn't know about the action details. * refactor: remove unused properties
321 lines
8.9 KiB
Go
321 lines
8.9 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/nektos/act/pkg/common"
|
|
"github.com/nektos/act/pkg/container"
|
|
"github.com/nektos/act/pkg/exprparser"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ExpressionEvaluator is the interface for evaluating expressions
|
|
type ExpressionEvaluator interface {
|
|
evaluate(context.Context, string, exprparser.DefaultStatusCheck) (interface{}, error)
|
|
EvaluateYamlNode(context.Context, *yaml.Node) error
|
|
Interpolate(context.Context, string) string
|
|
}
|
|
|
|
// NewExpressionEvaluator creates a new evaluator
|
|
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) 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
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
inputs := make(map[string]interface{})
|
|
for k, v := range rc.GetEnv() {
|
|
if strings.HasPrefix(k, "INPUT_") {
|
|
inputs[strings.ToLower(strings.TrimPrefix(k, "INPUT_"))] = v
|
|
}
|
|
}
|
|
|
|
ee := &exprparser.EvaluationEnvironment{
|
|
Github: rc.getGithubContext(ctx),
|
|
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",
|
|
"arch": container.RunnerArch(ctx),
|
|
"temp": "/tmp",
|
|
"tool_cache": "/opt/hostedtoolcache",
|
|
},
|
|
Secrets: rc.Config.Secrets,
|
|
Strategy: strategy,
|
|
Matrix: rc.Matrix,
|
|
Needs: using,
|
|
Inputs: inputs,
|
|
}
|
|
return expressionEvaluator{
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
Run: rc.Run,
|
|
WorkingDir: rc.Config.Workdir,
|
|
Context: "job",
|
|
}),
|
|
}
|
|
}
|
|
|
|
// NewExpressionEvaluator creates a new evaluator
|
|
func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) 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
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
inputs := make(map[string]interface{})
|
|
for k, v := range *step.getEnv() {
|
|
if strings.HasPrefix(k, "INPUT_") {
|
|
inputs[strings.ToLower(strings.TrimPrefix(k, "INPUT_"))] = v
|
|
}
|
|
}
|
|
|
|
ee := &exprparser.EvaluationEnvironment{
|
|
Github: step.getGithubContext(ctx),
|
|
Env: *step.getEnv(),
|
|
Job: rc.getJobContext(),
|
|
Steps: rc.getStepsContext(),
|
|
Runner: map[string]interface{}{
|
|
"os": "Linux",
|
|
"arch": container.RunnerArch(ctx),
|
|
"temp": "/tmp",
|
|
"tool_cache": "/opt/hostedtoolcache",
|
|
},
|
|
Secrets: rc.Config.Secrets,
|
|
Strategy: strategy,
|
|
Matrix: rc.Matrix,
|
|
Needs: using,
|
|
// todo: should be unavailable
|
|
// but required to interpolate/evaluate the inputs in actions/composite
|
|
Inputs: inputs,
|
|
}
|
|
return expressionEvaluator{
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
Run: rc.Run,
|
|
WorkingDir: rc.Config.Workdir,
|
|
Context: "step",
|
|
}),
|
|
}
|
|
}
|
|
|
|
type expressionEvaluator struct {
|
|
interpreter exprparser.Interpreter
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
|
|
logger := common.Logger(ctx)
|
|
logger.Debugf("evaluating expression '%s'", in)
|
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
|
|
|
printable := regexp.MustCompile(`::add-mask::.*`).ReplaceAllString(fmt.Sprintf("%t", evaluated), "::add-mask::***)")
|
|
logger.Debugf("expression '%s' evaluated to '%s'", in, printable)
|
|
|
|
return evaluated, err
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateScalarYamlNode(ctx context.Context, 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(ctx, in, false)
|
|
res, err := ee.evaluate(ctx, expr, exprparser.DefaultStatusCheckNone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return node.Encode(res)
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateMappingYamlNode(ctx context.Context, 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(ctx, 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(ctx, k); err != nil {
|
|
return err
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateSequenceYamlNode(ctx context.Context, 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(ctx, 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(ctx context.Context, node *yaml.Node) error {
|
|
switch node.Kind {
|
|
case yaml.ScalarNode:
|
|
return ee.evaluateScalarYamlNode(ctx, node)
|
|
case yaml.MappingNode:
|
|
return ee.evaluateMappingYamlNode(ctx, node)
|
|
case yaml.SequenceNode:
|
|
return ee.evaluateSequenceYamlNode(ctx, node)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (ee expressionEvaluator) Interpolate(ctx context.Context, in string) string {
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
return in
|
|
}
|
|
|
|
expr, _ := rewriteSubExpression(ctx, in, true)
|
|
evaluated, err := ee.evaluate(ctx, expr, exprparser.DefaultStatusCheckNone)
|
|
if err != nil {
|
|
common.Logger(ctx).Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
|
return ""
|
|
}
|
|
|
|
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(ctx context.Context, evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
|
|
nextExpr, _ := rewriteSubExpression(ctx, expr, false)
|
|
|
|
evaluated, err := evaluator.evaluate(ctx, nextExpr, defaultStatusCheck)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return exprparser.IsTruthy(evaluated), nil
|
|
}
|
|
|
|
func escapeFormatString(in string) string {
|
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func rewriteSubExpression(ctx context.Context, 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, ", "))
|
|
if in != out {
|
|
common.Logger(ctx).Debugf("expression '%s' rewritten to '%s'", in, out)
|
|
}
|
|
return out, nil
|
|
}
|