act/pkg/runner/expression.go

645 lines
15 KiB
Go
Raw Normal View History

package runner
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/robertkrimen/otto"
log "github.com/sirupsen/logrus"
)
var expressionPattern, operatorPattern *regexp.Regexp
func init() {
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
operatorPattern = regexp.MustCompile("^[!=><|&]+$")
}
// NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
vm := rc.newVM()
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
return &expressionEvaluator{
vm,
}
}
2020-02-23 17:01:25 -06:00
// NewExpressionEvaluator creates a new evaluator
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
vm := sc.RunContext.newVM()
configers := []func(*otto.Otto){
2020-02-23 17:01:25 -06:00
sc.vmEnv(),
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
sc.vmNeeds(),
sc.vmSuccess(),
sc.vmFailure(),
}
for _, configer := range configers {
configer(vm)
}
return &expressionEvaluator{
vm,
}
}
// ExpressionEvaluator is the interface for evaluating expressions
type ExpressionEvaluator interface {
Evaluate(string) (string, bool, error)
Interpolate(string) string
InterpolateWithStringCheck(string) (string, bool)
Rewrite(string) string
}
type expressionEvaluator struct {
vm *otto.Otto
}
func (ee *expressionEvaluator) Evaluate(in string) (string, bool, error) {
if strings.HasPrefix(in, `secrets.`) {
in = `secrets.` + strings.ToUpper(strings.SplitN(in, `.`, 2)[1])
}
re := ee.Rewrite(in)
if re != in {
log.Debugf("Evaluating '%s' instead of '%s'", re, in)
}
val, err := ee.vm.Run(re)
if err != nil {
return "", false, err
}
2020-03-12 19:22:33 -05:00
if val.IsNull() || val.IsUndefined() {
return "", false, nil
2020-03-12 19:22:33 -05:00
}
valAsString, err := val.ToString()
if err != nil {
return "", false, err
}
return valAsString, val.IsString(), err
}
func (ee *expressionEvaluator) Interpolate(in string) string {
interpolated, _ := ee.InterpolateWithStringCheck(in)
return interpolated
}
func (ee *expressionEvaluator) InterpolateWithStringCheck(in string) (string, bool) {
errList := make([]error, 0)
2020-03-12 19:22:33 -05:00
out := in
isString := false
2020-03-12 19:22:33 -05:00
for {
out = expressionPattern.ReplaceAllStringFunc(in, func(match string) string {
// Extract and trim the actual expression inside ${{...}} delimiters
expression := expressionPattern.ReplaceAllString(match, "$1")
// Evaluate the expression and retrieve errors if any
evaluated, evaluatedIsString, err := ee.Evaluate(expression)
2020-03-12 19:22:33 -05:00
if err != nil {
errList = append(errList, err)
}
isString = evaluatedIsString
2020-03-12 19:22:33 -05:00
return evaluated
})
if len(errList) > 0 {
log.Errorf("Unable to interpolate string '%s' - %v", in, errList)
2020-03-12 19:22:33 -05:00
break
}
2020-03-12 19:22:33 -05:00
if out == in {
// No replacement occurred, we're done!
2020-03-12 19:22:33 -05:00
break
}
in = out
}
return out, isString
}
// Rewrite tries to transform any javascript property accessor into its bracket notation.
// For instance, "object.property" would become "object['property']".
func (ee *expressionEvaluator) Rewrite(in string) string {
var buf strings.Builder
r := strings.NewReader(in)
for {
c, _, err := r.ReadRune()
if err == io.EOF {
break
}
//nolint
switch {
default:
buf.WriteRune(c)
case c == '\'':
buf.WriteRune(c)
ee.advString(&buf, r)
case c == '.':
buf.WriteString("['")
ee.advPropertyName(&buf, r)
buf.WriteString("']")
}
}
return buf.String()
}
func (*expressionEvaluator) advString(w *strings.Builder, r *strings.Reader) error {
for {
c, _, err := r.ReadRune()
if err != nil {
return err
}
if c != '\'' {
w.WriteRune(c)
continue
}
// Handles a escaped string: ex. 'It''s ok'
c, _, err = r.ReadRune()
if err != nil {
w.WriteString("'")
return err
}
if c != '\'' {
w.WriteString("'")
if err := r.UnreadRune(); err != nil {
return err
}
break
}
w.WriteString(`\'`)
}
return nil
}
func (*expressionEvaluator) advPropertyName(w *strings.Builder, r *strings.Reader) error {
for {
c, _, err := r.ReadRune()
if err != nil {
return err
}
if !isLetter(c) {
if err := r.UnreadRune(); err != nil {
return err
}
break
}
w.WriteRune(c)
}
return nil
}
func isLetter(c rune) bool {
switch {
case c >= 'a' && c <= 'z':
return true
case c >= 'A' && c <= 'Z':
return true
case c >= '0' && c <= '9':
return true
case c == '_' || c == '-':
return true
default:
return false
}
}
func (rc *RunContext) newVM() *otto.Otto {
configers := []func(*otto.Otto){
vmContains,
vmStartsWith,
vmEndsWith,
vmFormat,
vmJoin,
vmToJSON,
2020-09-01 15:55:29 -05:00
vmFromJSON,
vmAlways,
rc.vmCancelled(),
rc.vmSuccess(),
rc.vmFailure(),
rc.vmHashFiles(),
rc.vmGithub(),
rc.vmJob(),
rc.vmSteps(),
rc.vmRunner(),
rc.vmSecrets(),
rc.vmStrategy(),
rc.vmMatrix(),
rc.vmEnv(),
rc.vmNeeds(),
Feature: uses in composite (#793) * Feature: uses in composite * Negate logic * Reduce complexity * Update step_context.go * Update step_context.go * Update step_context.go * Fix syntax error in test * Bump * Disable usage of actions/setup-node@v2 * Bump * Fix step id collision * Fix output command workaround * Make secrets context inaccessible in composite * Fix order after adding a workaround (needs tests) Fixes https://github.com/nektos/act/pull/793#issuecomment-922329838 * Evaluate env before passing one step deeper If env would contain any inputs, steps ctx or secrets there was undefined behaviour * [no ci] prepare secret test * Initial test pass inputs as env * Fix syntax error * extend test also for direct invoke * Fix passing provided env as composite output * Fix syntax error * toUpper 'no such secret', act has a bug * fix indent * Fix env outputs in composite * Test env outputs of composite * Fix inputs not defined in docker actions * Fix interpolate args input of docker actions * Fix lint * AllowCompositeIf now defaults to true see https://github.com/actions/runner/releases/tag/v2.284.0 * Fix lint * Fix env of docker action.yml * Test calling a local docker action from composite With input context hirachy * local-action-dockerfile Test pass on action/runner It seems action/runner ignores overrides of args, if the target docker action has the args property set. * Fix exec permissions of docker-local-noargs * Revert getStepsContext change * fix: handle composite action on error and continue This change is a follow up of https://github.com/nektos/act/pull/840 and integrates with https://github.com/nektos/act/pull/793 There are two things included here: - The default value for a step.if in an action need to be 'success()' - We need to hand the error from a composite action back to the calling executor Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> * Patch inputs can be bool, float64 and string for workflow_call Also inputs is now always defined, but may be null * Simplify cherry-picked commit * Minor style adjustments * Remove chmod +x from tests now fails on windows like before * Fix GITHUB_ACTION_PATH some action env vars Fixes GITHUB_ACTION_REPOSITORY, GITHUB_ACTION_REF. * Add comment to CompositeRestrictions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Ryan <me@hackerc.at> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2021-12-22 13:19:50 -06:00
rc.vmInputs(),
}
vm := otto.New()
for _, configer := range configers {
configer(vm)
}
return vm
}
func vmContains(vm *otto.Otto) {
_ = vm.Set("contains", func(searchString interface{}, searchValue string) bool {
if searchStringString, ok := searchString.(string); ok {
return strings.Contains(strings.ToLower(searchStringString), strings.ToLower(searchValue))
} else if searchStringArray, ok := searchString.([]string); ok {
for _, s := range searchStringArray {
if strings.EqualFold(s, searchValue) {
return true
}
}
}
return false
})
}
func vmStartsWith(vm *otto.Otto) {
_ = vm.Set("startsWith", func(searchString string, searchValue string) bool {
return strings.HasPrefix(strings.ToLower(searchString), strings.ToLower(searchValue))
})
}
func vmEndsWith(vm *otto.Otto) {
_ = vm.Set("endsWith", func(searchString string, searchValue string) bool {
return strings.HasSuffix(strings.ToLower(searchString), strings.ToLower(searchValue))
})
}
func vmFormat(vm *otto.Otto) {
_ = vm.Set("format", func(s string, vals ...otto.Value) string {
ex := regexp.MustCompile(`(\{[0-9]+\}|\{.?|\}.?)`)
return ex.ReplaceAllStringFunc(s, func(seg string) string {
switch seg {
case "{{":
return "{"
case "}}":
return "}"
default:
if len(seg) < 3 || !strings.HasPrefix(seg, "{") {
log.Errorf("The following format string is invalid: '%v'", s)
return ""
}
_i := seg[1 : len(seg)-1]
i, err := strconv.ParseInt(_i, 10, 32)
if err != nil {
log.Errorf("The following format string is invalid: '%v'. Error: %v", s, err)
return ""
}
if i >= int64(len(vals)) {
log.Errorf("The following format string references more arguments than were supplied: '%v'", s)
return ""
}
if vals[i].IsNull() || vals[i].IsUndefined() {
return ""
}
return vals[i].String()
}
})
})
}
func vmJoin(vm *otto.Otto) {
_ = vm.Set("join", func(element interface{}, optionalElem string) string {
slist := make([]string, 0)
if elementString, ok := element.(string); ok {
slist = append(slist, elementString)
} else if elementArray, ok := element.([]string); ok {
slist = append(slist, elementArray...)
}
if optionalElem != "" {
slist = append(slist, optionalElem)
}
return strings.Join(slist, " ")
})
}
func vmToJSON(vm *otto.Otto) {
toJSON := func(o interface{}) string {
rtn, err := json.MarshalIndent(o, "", " ")
if err != nil {
log.Errorf("Unable to marshal: %v", err)
return ""
}
return string(rtn)
}
_ = vm.Set("toJSON", toJSON)
_ = vm.Set("toJson", toJSON)
}
2020-09-01 15:55:29 -05:00
func vmFromJSON(vm *otto.Otto) {
fromJSON := func(str string) interface{} {
var dat interface{}
2020-09-01 15:55:29 -05:00
err := json.Unmarshal([]byte(str), &dat)
if err != nil {
log.Errorf("Unable to unmarshal: %v", err)
2020-09-01 15:55:29 -05:00
return dat
}
return dat
}
_ = vm.Set("fromJSON", fromJSON)
_ = vm.Set("fromJson", fromJSON)
}
func (rc *RunContext) vmHashFiles() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("hashFiles", func(paths ...string) string {
var files []string
for i := range paths {
newFiles, err := filepath.Glob(filepath.Join(rc.Config.Workdir, paths[i]))
if err != nil {
log.Errorf("Unable to glob.Glob: %v", err)
return ""
}
files = append(files, newFiles...)
}
hasher := sha256.New()
for _, file := range files {
f, err := os.Open(file)
if err != nil {
log.Errorf("Unable to os.Open: %v", err)
}
if _, err := io.Copy(hasher, f); err != nil {
log.Errorf("Unable to io.Copy: %v", err)
}
if err := f.Close(); err != nil {
log.Errorf("Unable to Close file: %v", err)
}
}
return hex.EncodeToString(hasher.Sum(nil))
})
}
}
func (rc *RunContext) vmSuccess() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("success", func() bool {
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
for _, needs := range jobNeeds {
if jobs[needs].Result != "success" {
return false
}
}
return true
})
}
}
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
func (rc *RunContext) vmFailure() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("failure", func() bool {
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
for _, needs := range jobNeeds {
if jobs[needs].Result == "failure" {
return true
}
}
return false
})
}
}
func vmAlways(vm *otto.Otto) {
_ = vm.Set("always", func() bool {
return true
})
}
func (rc *RunContext) vmCancelled() func(vm *otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("cancelled", func() bool {
return rc.getJobContext().Status == "cancelled"
})
}
}
func (rc *RunContext) vmGithub() func(*otto.Otto) {
github := rc.getGithubContext()
return func(vm *otto.Otto) {
_ = vm.Set("github", github)
}
}
func (rc *RunContext) vmEnv() func(*otto.Otto) {
return func(vm *otto.Otto) {
env := rc.GetEnv()
log.Debugf("context env => %v", env)
_ = vm.Set("env", env)
}
}
2020-02-23 17:01:25 -06:00
func (sc *StepContext) vmEnv() func(*otto.Otto) {
return func(vm *otto.Otto) {
log.Debugf("context env => %v", sc.Env)
2020-02-23 17:01:25 -06:00
_ = vm.Set("env", sc.Env)
}
}
Feature: uses in composite (#793) * Feature: uses in composite * Negate logic * Reduce complexity * Update step_context.go * Update step_context.go * Update step_context.go * Fix syntax error in test * Bump * Disable usage of actions/setup-node@v2 * Bump * Fix step id collision * Fix output command workaround * Make secrets context inaccessible in composite * Fix order after adding a workaround (needs tests) Fixes https://github.com/nektos/act/pull/793#issuecomment-922329838 * Evaluate env before passing one step deeper If env would contain any inputs, steps ctx or secrets there was undefined behaviour * [no ci] prepare secret test * Initial test pass inputs as env * Fix syntax error * extend test also for direct invoke * Fix passing provided env as composite output * Fix syntax error * toUpper 'no such secret', act has a bug * fix indent * Fix env outputs in composite * Test env outputs of composite * Fix inputs not defined in docker actions * Fix interpolate args input of docker actions * Fix lint * AllowCompositeIf now defaults to true see https://github.com/actions/runner/releases/tag/v2.284.0 * Fix lint * Fix env of docker action.yml * Test calling a local docker action from composite With input context hirachy * local-action-dockerfile Test pass on action/runner It seems action/runner ignores overrides of args, if the target docker action has the args property set. * Fix exec permissions of docker-local-noargs * Revert getStepsContext change * fix: handle composite action on error and continue This change is a follow up of https://github.com/nektos/act/pull/840 and integrates with https://github.com/nektos/act/pull/793 There are two things included here: - The default value for a step.if in an action need to be 'success()' - We need to hand the error from a composite action back to the calling executor Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> * Patch inputs can be bool, float64 and string for workflow_call Also inputs is now always defined, but may be null * Simplify cherry-picked commit * Minor style adjustments * Remove chmod +x from tests now fails on windows like before * Fix GITHUB_ACTION_PATH some action env vars Fixes GITHUB_ACTION_REPOSITORY, GITHUB_ACTION_REF. * Add comment to CompositeRestrictions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Ryan <me@hackerc.at> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2021-12-22 13:19:50 -06:00
func (rc *RunContext) vmInputs() func(*otto.Otto) {
2020-02-24 00:34:48 -06:00
return func(vm *otto.Otto) {
Feature: uses in composite (#793) * Feature: uses in composite * Negate logic * Reduce complexity * Update step_context.go * Update step_context.go * Update step_context.go * Fix syntax error in test * Bump * Disable usage of actions/setup-node@v2 * Bump * Fix step id collision * Fix output command workaround * Make secrets context inaccessible in composite * Fix order after adding a workaround (needs tests) Fixes https://github.com/nektos/act/pull/793#issuecomment-922329838 * Evaluate env before passing one step deeper If env would contain any inputs, steps ctx or secrets there was undefined behaviour * [no ci] prepare secret test * Initial test pass inputs as env * Fix syntax error * extend test also for direct invoke * Fix passing provided env as composite output * Fix syntax error * toUpper 'no such secret', act has a bug * fix indent * Fix env outputs in composite * Test env outputs of composite * Fix inputs not defined in docker actions * Fix interpolate args input of docker actions * Fix lint * AllowCompositeIf now defaults to true see https://github.com/actions/runner/releases/tag/v2.284.0 * Fix lint * Fix env of docker action.yml * Test calling a local docker action from composite With input context hirachy * local-action-dockerfile Test pass on action/runner It seems action/runner ignores overrides of args, if the target docker action has the args property set. * Fix exec permissions of docker-local-noargs * Revert getStepsContext change * fix: handle composite action on error and continue This change is a follow up of https://github.com/nektos/act/pull/840 and integrates with https://github.com/nektos/act/pull/793 There are two things included here: - The default value for a step.if in an action need to be 'success()' - We need to hand the error from a composite action back to the calling executor Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> * Patch inputs can be bool, float64 and string for workflow_call Also inputs is now always defined, but may be null * Simplify cherry-picked commit * Minor style adjustments * Remove chmod +x from tests now fails on windows like before * Fix GITHUB_ACTION_PATH some action env vars Fixes GITHUB_ACTION_REPOSITORY, GITHUB_ACTION_REF. * Add comment to CompositeRestrictions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Ryan <me@hackerc.at> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2021-12-22 13:19:50 -06:00
_ = vm.Set("inputs", rc.Inputs)
2020-02-24 00:34:48 -06:00
}
}
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
func (sc *StepContext) vmNeeds() func(*otto.Otto) {
jobs := sc.RunContext.Run.Workflow.Jobs
jobNeeds := sc.RunContext.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,
}
}
return func(vm *otto.Otto) {
log.Debugf("context needs => %v", using)
_ = vm.Set("needs", using)
}
}
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
func (sc *StepContext) vmSuccess() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("success", func() bool {
return sc.RunContext.getJobContext().Status == "success"
})
}
}
func (sc *StepContext) vmFailure() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("failure", func() bool {
return sc.RunContext.getJobContext().Status == "failure"
})
}
}
type vmNeedsStruct struct {
Outputs map[string]string `json:"outputs"`
Result string `json:"result"`
}
func (rc *RunContext) vmNeeds() func(*otto.Otto) {
return func(vm *otto.Otto) {
needsFunc := func() otto.Value {
jobs := rc.Run.Workflow.Jobs
jobNeeds := rc.Run.Job().Needs()
using := make(map[string]vmNeedsStruct)
for _, needs := range jobNeeds {
using[needs] = vmNeedsStruct{
Outputs: jobs[needs].Outputs,
Result: jobs[needs].Result,
}
}
log.Debugf("context needs => %+v", using)
value, err := vm.ToValue(using)
if err != nil {
return vm.MakeTypeError(err.Error())
}
return value
}
// Results might change after the Otto VM was created
// and initialized. To access the current state
// we can't just pass a copy to Otto - instead we
// created a 'live-binding'.
// Technical Note: We don't want to pollute the global
// js namespace (and add things github actions hasn't)
// we delete the helper function after installing it
// as a getter.
global, _ := vm.Run("this")
_ = global.Object().Set("__needs__", needsFunc)
_, _ = vm.Run(`
(function (global) {
Object.defineProperty(global, 'needs', { get: global.__needs__ });
delete global.__needs__;
})(this)
`)
}
}
func (rc *RunContext) vmJob() func(*otto.Otto) {
job := rc.getJobContext()
return func(vm *otto.Otto) {
_ = vm.Set("job", job)
}
}
func (rc *RunContext) vmSteps() func(*otto.Otto) {
ctxSteps := rc.getStepsContext()
steps := make(map[string]interface{})
for id, ctxStep := range ctxSteps {
steps[id] = map[string]interface{}{
"conclusion": ctxStep.Conclusion.String(),
"outcome": ctxStep.Outcome.String(),
"outputs": ctxStep.Outputs,
}
}
return func(vm *otto.Otto) {
log.Debugf("context steps => %v", steps)
_ = vm.Set("steps", steps)
}
}
func (rc *RunContext) vmRunner() func(*otto.Otto) {
runner := map[string]interface{}{
"os": "Linux",
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
}
return func(vm *otto.Otto) {
_ = vm.Set("runner", runner)
}
}
func (rc *RunContext) vmSecrets() func(*otto.Otto) {
return func(vm *otto.Otto) {
Feature: uses in composite (#793) * Feature: uses in composite * Negate logic * Reduce complexity * Update step_context.go * Update step_context.go * Update step_context.go * Fix syntax error in test * Bump * Disable usage of actions/setup-node@v2 * Bump * Fix step id collision * Fix output command workaround * Make secrets context inaccessible in composite * Fix order after adding a workaround (needs tests) Fixes https://github.com/nektos/act/pull/793#issuecomment-922329838 * Evaluate env before passing one step deeper If env would contain any inputs, steps ctx or secrets there was undefined behaviour * [no ci] prepare secret test * Initial test pass inputs as env * Fix syntax error * extend test also for direct invoke * Fix passing provided env as composite output * Fix syntax error * toUpper 'no such secret', act has a bug * fix indent * Fix env outputs in composite * Test env outputs of composite * Fix inputs not defined in docker actions * Fix interpolate args input of docker actions * Fix lint * AllowCompositeIf now defaults to true see https://github.com/actions/runner/releases/tag/v2.284.0 * Fix lint * Fix env of docker action.yml * Test calling a local docker action from composite With input context hirachy * local-action-dockerfile Test pass on action/runner It seems action/runner ignores overrides of args, if the target docker action has the args property set. * Fix exec permissions of docker-local-noargs * Revert getStepsContext change * fix: handle composite action on error and continue This change is a follow up of https://github.com/nektos/act/pull/840 and integrates with https://github.com/nektos/act/pull/793 There are two things included here: - The default value for a step.if in an action need to be 'success()' - We need to hand the error from a composite action back to the calling executor Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> * Patch inputs can be bool, float64 and string for workflow_call Also inputs is now always defined, but may be null * Simplify cherry-picked commit * Minor style adjustments * Remove chmod +x from tests now fails on windows like before * Fix GITHUB_ACTION_PATH some action env vars Fixes GITHUB_ACTION_REPOSITORY, GITHUB_ACTION_REF. * Add comment to CompositeRestrictions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Ryan <me@hackerc.at> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2021-12-22 13:19:50 -06:00
// Hide secrets from composite actions
if rc.Composite == nil {
_ = vm.Set("secrets", rc.Config.Secrets)
}
}
}
func (rc *RunContext) vmStrategy() func(*otto.Otto) {
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
}
return func(vm *otto.Otto) {
_ = vm.Set("strategy", strategy)
}
}
func (rc *RunContext) vmMatrix() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("matrix", rc.Matrix)
}
}
fix: continue jobs + steps after failure (#840) * fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 14:57:42 -06:00
// EvalBool evaluates an expression against given evaluator
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
if splitPattern == nil {
splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
}
if strings.HasPrefix(strings.TrimSpace(expr), "!") {
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
}
if expr != "" {
parts := splitPattern.FindAllString(expr, -1)
var evaluatedParts []string
for i, part := range parts {
if operatorPattern.MatchString(part) {
evaluatedParts = append(evaluatedParts, part)
continue
}
interpolatedPart, isString := evaluator.InterpolateWithStringCheck(part)
// This peculiar transformation has to be done because the GitHub parser
// treats false returned from contexts as a string, not a boolean.
// Hence env.SOMETHING will be evaluated to true in an if: expression
// regardless if SOMETHING is set to false, true or any other string.
// It also handles some other weirdness that I found by trial and error.
if (expressionPattern.MatchString(part) && // it is an expression
!strings.Contains(part, "!")) && // but it's not negated
interpolatedPart == "false" && // and the interpolated string is false
(isString || previousOrNextPartIsAnOperator(i, parts)) { // and it's of type string or has an logical operator before or after
interpolatedPart = fmt.Sprintf("'%s'", interpolatedPart) // then we have to quote the false expression
}
evaluatedParts = append(evaluatedParts, interpolatedPart)
}
joined := strings.Join(evaluatedParts, " ")
v, _, err := evaluator.Evaluate(fmt.Sprintf("Boolean(%s)", joined))
if err != nil {
return false, err
}
log.Debugf("expression '%s' evaluated to '%s'", expr, v)
return v == "true", nil
}
return true, nil
}