act/pkg/runner/step_run.go

222 lines
6.2 KiB
Go

package runner
import (
"context"
"fmt"
"runtime"
"strings"
"github.com/kballard/go-shellquote"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/lookpath"
"github.com/nektos/act/pkg/model"
)
type stepRun struct {
Step *model.Step
RunContext *RunContext
cmd []string
cmdline string
env map[string]string
WorkingDirectory string
}
func (sr *stepRun) pre() common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func (sr *stepRun) main() common.Executor {
sr.env = map[string]string{}
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
sr.setupShellCommandExecutor(),
func(ctx context.Context) error {
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil {
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
}
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
},
))
}
func (sr *stepRun) post() common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func (sr *stepRun) getRunContext() *RunContext {
return sr.RunContext
}
func (sr *stepRun) getGithubContext(ctx context.Context) *model.GithubContext {
return sr.getRunContext().getGithubContext(ctx)
}
func (sr *stepRun) getStepModel() *model.Step {
return sr.Step
}
func (sr *stepRun) getEnv() *map[string]string {
return &sr.env
}
func (sr *stepRun) getIfExpression(_ context.Context, _ stepStage) string {
return sr.Step.If.Value
}
func (sr *stepRun) setupShellCommandExecutor() common.Executor {
return func(ctx context.Context) error {
scriptName, script, err := sr.setupShellCommand(ctx)
if err != nil {
return err
}
rc := sr.getRunContext()
return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
Name: scriptName,
Mode: 0o755,
Body: script,
})(ctx)
}
}
func getScriptName(rc *RunContext, step *model.Step) string {
scriptName := step.ID
for rcs := rc; rcs.Parent != nil; rcs = rcs.Parent {
scriptName = fmt.Sprintf("%s-composite-%s", rcs.Parent.CurrentStep, scriptName)
}
return fmt.Sprintf("workflow/%s", scriptName)
}
// TODO: Currently we just ignore top level keys, BUT we should return proper error on them
// BUTx2 I leave this for when we rewrite act to use actionlint for workflow validation
// so we return proper errors before any execution or spawning containers
// it will error anyway with:
// OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "${{": executable file not found in $PATH: unknown
func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string, err error) {
logger := common.Logger(ctx)
sr.setupShell(ctx)
sr.setupWorkingDirectory(ctx)
step := sr.Step
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
scCmd := step.ShellCommand()
name = getScriptName(sr.RunContext, step)
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L47-L64
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L19-L27
runPrepend := ""
runAppend := ""
switch step.Shell {
case "bash", "sh":
name += ".sh"
case "pwsh", "powershell":
name += ".ps1"
runPrepend = "$ErrorActionPreference = 'stop'"
runAppend = "if ((Test-Path -LiteralPath variable:/LASTEXITCODE)) { exit $LASTEXITCODE }"
case "cmd":
name += ".cmd"
runPrepend = "@echo off"
case "python":
name += ".py"
}
script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend)
if !strings.Contains(script, "::add-mask::") && !sr.RunContext.Config.InsecureSecrets {
logger.Debugf("Wrote command \n%s\n to '%s'", script, name)
} else {
logger.Debugf("Wrote add-mask command to '%s'", name)
}
rc := sr.getRunContext()
scriptPath := fmt.Sprintf("%s/%s", rc.JobContainer.GetActPath(), name)
sr.cmdline = strings.Replace(scCmd, `{0}`, scriptPath, 1)
sr.cmd, err = shellquote.Split(sr.cmdline)
return name, script, err
}
type localEnv struct {
env map[string]string
}
func (l *localEnv) Getenv(name string) string {
if runtime.GOOS == "windows" {
for k, v := range l.env {
if strings.EqualFold(name, k) {
return v
}
}
return ""
}
return l.env[name]
}
func (sr *stepRun) setupShell(ctx context.Context) {
rc := sr.RunContext
step := sr.Step
if step.Shell == "" {
step.Shell = rc.Run.Job().Defaults.Run.Shell
}
step.Shell = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, step.Shell)
if step.Shell == "" {
step.Shell = rc.Run.Workflow.Defaults.Run.Shell
}
if step.Shell == "" {
if _, ok := rc.JobContainer.(*container.HostEnvironment); ok {
shellWithFallback := []string{"bash", "sh"}
// Don't use bash on windows by default, if not using a docker container
if runtime.GOOS == "windows" {
shellWithFallback = []string{"pwsh", "powershell"}
}
step.Shell = shellWithFallback[0]
lenv := &localEnv{env: map[string]string{}}
for k, v := range sr.env {
lenv.env[k] = v
}
sr.getRunContext().ApplyExtraPath(ctx, &lenv.env)
_, err := lookpath.LookPath2(shellWithFallback[0], lenv)
if err != nil {
step.Shell = shellWithFallback[1]
}
} else if containerImage := rc.containerImage(ctx); containerImage != "" {
// Currently only linux containers are supported, use sh by default like actions/runner
step.Shell = "sh"
}
}
}
func (sr *stepRun) setupWorkingDirectory(ctx context.Context) {
rc := sr.RunContext
step := sr.Step
workingdirectory := ""
if step.WorkingDirectory == "" {
workingdirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
} else {
workingdirectory = step.WorkingDirectory
}
// jobs can receive context values, so we interpolate
workingdirectory = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, workingdirectory)
// but top level keys in workflow file like `defaults` or `env` can't
if workingdirectory == "" {
workingdirectory = rc.Run.Workflow.Defaults.Run.WorkingDirectory
}
sr.WorkingDirectory = workingdirectory
}