943a0e6eea
* feat: add post step to actions and add state command This commit includes requried changes for running post steps for local and remote actions. This allows general cleanup work to be done after executing an action. Communication is allowed between this steps, by using the action state. * feat: collect pre and post steps for composite actions * refactor: move composite action logic into own file * refactor: restructure composite handling * feat: run composite post steps during post step lifecycle * refactor: remove duplicate log output * feat: run all composite post actions in a step Since composite actions could have multiple pre/post steps inside, we need to run all of them in a single top-level pre/post step. This PR includes a test case for this and the correct order of steps to be executed. * refactor: remove unused lines of code * refactor: simplify test expression * fix: use composite job logger * fix: make step output more readable * fix: enforce running all post executor To make sure every post executor/step is executed, it is chained with it's own Finally executor. * fix: do not run post step if no step result is available Having no step result means we do not run any step (neither pre nor main) and we do not need to run post. * fix: setup defaults If no pre-if or post-if is given, it should default to 'always()'. This could be set even if there is no pre or post step. In fact this is required for composite actions and included post steps to run. * fix: output step related if expression * test: update expectation * feat: run pre step from actions (#1110) This PR implements running pre steps for remote actions. This includes remote actions using inside local composite actions. * fix: set correct expr default status checks For post-if conditions the default status check should be always(), while for all other if expression the default status check is success() References: https://docs.github.com/en/actions/learn-github-actions/expressions#status-check-functions https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspost-if * fix: remove code added during rebase
299 lines
7.9 KiB
Go
299 lines
7.9 KiB
Go
package runner
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/nektos/act/pkg/exprparser"
|
|
log "github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ExpressionEvaluator is the interface for evaluating expressions
|
|
type ExpressionEvaluator interface {
|
|
evaluate(string, exprparser.DefaultStatusCheck) (interface{}, error)
|
|
EvaluateYamlNode(node *yaml.Node) error
|
|
Interpolate(string) string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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: rc.Config.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",
|
|
}),
|
|
}
|
|
}
|
|
|
|
// NewExpressionEvaluator creates a new evaluator
|
|
func (rc *RunContext) NewStepExpressionEvaluator(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,
|
|
}
|
|
}
|
|
|
|
ee := &exprparser.EvaluationEnvironment{
|
|
Github: rc.getGithubContext(),
|
|
Env: *step.getEnv(),
|
|
Job: rc.getJobContext(),
|
|
Steps: rc.getStepsContext(),
|
|
Runner: map[string]interface{}{
|
|
"os": "Linux",
|
|
"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: rc.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(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
|
|
log.Debugf("evaluating expression '%s'", in)
|
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
|
log.Debugf("expression '%s' evaluated to '%t'", in, evaluated)
|
|
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 {
|
|
log.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(evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
|
|
nextExpr, _ := rewriteSubExpression(expr, false)
|
|
|
|
evaluated, err := evaluator.evaluate(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(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 {
|
|
log.Debugf("expression '%s' rewritten to '%s'", in, out)
|
|
}
|
|
return out, nil
|
|
}
|