diff options
Diffstat (limited to 'pkg/runner/step.go')
-rw-r--r-- | pkg/runner/step.go | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/pkg/runner/step.go b/pkg/runner/step.go new file mode 100644 index 0000000..c67b5b0 --- /dev/null +++ b/pkg/runner/step.go @@ -0,0 +1,322 @@ +package runner + +import ( + "context" + "fmt" + "path" + "strconv" + "strings" + "time" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/container" + "github.com/nektos/act/pkg/exprparser" + "github.com/nektos/act/pkg/model" +) + +type step interface { + pre() common.Executor + main() common.Executor + post() common.Executor + + getRunContext() *RunContext + getGithubContext(ctx context.Context) *model.GithubContext + getStepModel() *model.Step + getEnv() *map[string]string + getIfExpression(context context.Context, stage stepStage) string +} + +type stepStage int + +const ( + stepStagePre stepStage = iota + stepStageMain + stepStagePost +) + +// Controls how many symlinks are resolved for local and remote Actions +const maxSymlinkDepth = 10 + +func (s stepStage) String() string { + switch s { + case stepStagePre: + return "Pre" + case stepStageMain: + return "Main" + case stepStagePost: + return "Post" + } + return "Unknown" +} + +func processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error { + env := map[string]string{} + err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx) + if err != nil { + return err + } + for k, v := range env { + setter(ctx, map[string]string{"name": k}, v) + } + return nil +} + +func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + rc := step.getRunContext() + stepModel := step.getStepModel() + + ifExpression := step.getIfExpression(ctx, stage) + rc.CurrentStep = stepModel.ID + + stepResult := &model.StepResult{ + Outcome: model.StepStatusSuccess, + Conclusion: model.StepStatusSuccess, + Outputs: make(map[string]string), + } + if stage == stepStageMain { + rc.StepResults[rc.CurrentStep] = stepResult + } + + err := setupEnv(ctx, step) + if err != nil { + return err + } + + runStep, err := isStepEnabled(ctx, ifExpression, step, stage) + if err != nil { + stepResult.Conclusion = model.StepStatusFailure + stepResult.Outcome = model.StepStatusFailure + return err + } + + if !runStep { + stepResult.Conclusion = model.StepStatusSkipped + stepResult.Outcome = model.StepStatusSkipped + logger.WithField("stepResult", stepResult.Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression) + return nil + } + + stepString := rc.ExprEval.Interpolate(ctx, stepModel.String()) + if strings.Contains(stepString, "::add-mask::") { + stepString = "add-mask command" + } + logger.Infof("\u2B50 Run %s %s", stage, stepString) + + // Prepare and clean Runner File Commands + actPath := rc.JobContainer.GetActPath() + + outputFileCommand := path.Join("workflow", "outputcmd.txt") + (*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand) + + stateFileCommand := path.Join("workflow", "statecmd.txt") + (*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand) + + pathFileCommand := path.Join("workflow", "pathcmd.txt") + (*step.getEnv())["GITHUB_PATH"] = path.Join(actPath, pathFileCommand) + + envFileCommand := path.Join("workflow", "envs.txt") + (*step.getEnv())["GITHUB_ENV"] = path.Join(actPath, envFileCommand) + + summaryFileCommand := path.Join("workflow", "SUMMARY.md") + (*step.getEnv())["GITHUB_STEP_SUMMARY"] = path.Join(actPath, summaryFileCommand) + + _ = rc.JobContainer.Copy(actPath, &container.FileEntry{ + Name: outputFileCommand, + Mode: 0o666, + }, &container.FileEntry{ + Name: stateFileCommand, + Mode: 0o666, + }, &container.FileEntry{ + Name: pathFileCommand, + Mode: 0o666, + }, &container.FileEntry{ + Name: envFileCommand, + Mode: 0666, + }, &container.FileEntry{ + Name: summaryFileCommand, + Mode: 0o666, + })(ctx) + + timeoutctx, cancelTimeOut := evaluateStepTimeout(ctx, rc.ExprEval, stepModel) + defer cancelTimeOut() + err = executor(timeoutctx) + + if err == nil { + logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString) + } else { + stepResult.Outcome = model.StepStatusFailure + + continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage) + if parseErr != nil { + stepResult.Conclusion = model.StepStatusFailure + return parseErr + } + + if continueOnError { + logger.Infof("Failed but continue next step") + err = nil + stepResult.Conclusion = model.StepStatusSuccess + } else { + stepResult.Conclusion = model.StepStatusFailure + } + + logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString) + } + // Process Runner File Commands + orgerr := err + err = processRunnerEnvFileCommand(ctx, envFileCommand, rc, rc.setEnv) + if err != nil { + return err + } + err = processRunnerEnvFileCommand(ctx, stateFileCommand, rc, rc.saveState) + if err != nil { + return err + } + err = processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput) + if err != nil { + return err + } + err = rc.UpdateExtraPath(ctx, path.Join(actPath, pathFileCommand)) + if err != nil { + return err + } + if orgerr != nil { + return orgerr + } + return err + } +} + +func evaluateStepTimeout(ctx context.Context, exprEval ExpressionEvaluator, stepModel *model.Step) (context.Context, context.CancelFunc) { + timeout := exprEval.Interpolate(ctx, stepModel.TimeoutMinutes) + if timeout != "" { + if timeOutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil { + return context.WithTimeout(ctx, time.Duration(timeOutMinutes)*time.Minute) + } + } + return ctx, func() {} +} + +func setupEnv(ctx context.Context, step step) error { + rc := step.getRunContext() + + mergeEnv(ctx, step) + // merge step env last, since it should not be overwritten + mergeIntoMap(step, step.getEnv(), step.getStepModel().GetEnv()) + + exprEval := rc.NewExpressionEvaluator(ctx) + for k, v := range *step.getEnv() { + if !strings.HasPrefix(k, "INPUT_") { + (*step.getEnv())[k] = exprEval.Interpolate(ctx, v) + } + } + // after we have an evaluated step context, update the expressions evaluator with a new env context + // you can use step level env in the with property of a uses construct + exprEval = rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv()) + for k, v := range *step.getEnv() { + if strings.HasPrefix(k, "INPUT_") { + (*step.getEnv())[k] = exprEval.Interpolate(ctx, v) + } + } + + common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv()) + + return nil +} + +func mergeEnv(ctx context.Context, step step) { + env := step.getEnv() + rc := step.getRunContext() + job := rc.Run.Job() + + c := job.Container() + if c != nil { + mergeIntoMap(step, env, rc.GetEnv(), c.Env) + } else { + mergeIntoMap(step, env, rc.GetEnv()) + } + + rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env) +} + +func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage) (bool, error) { + rc := step.getRunContext() + + var defaultStatusCheck exprparser.DefaultStatusCheck + if stage == stepStagePost { + defaultStatusCheck = exprparser.DefaultStatusCheckAlways + } else { + defaultStatusCheck = exprparser.DefaultStatusCheckSuccess + } + + runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck) + if err != nil { + return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err) + } + + return runStep, nil +} + +func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage) (bool, error) { + // https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=true#L962 + if len(strings.TrimSpace(expr)) == 0 { + return false, nil + } + + rc := step.getRunContext() + + continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone) + if err != nil { + return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err) + } + + return continueOnError, nil +} + +func mergeIntoMap(step step, target *map[string]string, maps ...map[string]string) { + if rc := step.getRunContext(); rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive() { + mergeIntoMapCaseInsensitive(*target, maps...) + } else { + mergeIntoMapCaseSensitive(*target, maps...) + } +} + +func mergeIntoMapCaseSensitive(target map[string]string, maps ...map[string]string) { + for _, m := range maps { + for k, v := range m { + target[k] = v + } + } +} + +func mergeIntoMapCaseInsensitive(target map[string]string, maps ...map[string]string) { + foldKeys := make(map[string]string, len(target)) + for k := range target { + foldKeys[strings.ToLower(k)] = k + } + toKey := func(s string) string { + foldKey := strings.ToLower(s) + if k, ok := foldKeys[foldKey]; ok { + return k + } + foldKeys[strings.ToLower(foldKey)] = s + return s + } + for _, m := range maps { + for k, v := range m { + target[toKey(k)] = v + } + } +} + +func symlinkJoin(filename, sym, parent string) (string, error) { + dir := path.Dir(filename) + dest := path.Join(dir, sym) + prefix := path.Clean(parent) + "/" + if strings.HasPrefix(dest, prefix) || prefix == "./" { + return dest, nil + } + return "", fmt.Errorf("symlink tries to access file '%s' outside of '%s'", strings.ReplaceAll(dest, "'", "''"), strings.ReplaceAll(parent, "'", "''")) +} |