github.com/nektos/act@v0.2.63/pkg/runner/step_run.go (about) 1 package runner 2 3 import ( 4 "context" 5 "fmt" 6 "runtime" 7 "strings" 8 9 "github.com/kballard/go-shellquote" 10 11 "github.com/nektos/act/pkg/common" 12 "github.com/nektos/act/pkg/container" 13 "github.com/nektos/act/pkg/lookpath" 14 "github.com/nektos/act/pkg/model" 15 ) 16 17 type stepRun struct { 18 Step *model.Step 19 RunContext *RunContext 20 cmd []string 21 cmdline string 22 env map[string]string 23 WorkingDirectory string 24 } 25 26 func (sr *stepRun) pre() common.Executor { 27 return func(ctx context.Context) error { 28 return nil 29 } 30 } 31 32 func (sr *stepRun) main() common.Executor { 33 sr.env = map[string]string{} 34 return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor( 35 sr.setupShellCommandExecutor(), 36 func(ctx context.Context) error { 37 sr.getRunContext().ApplyExtraPath(ctx, &sr.env) 38 if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil { 39 return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx) 40 } 41 return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx) 42 }, 43 )) 44 } 45 46 func (sr *stepRun) post() common.Executor { 47 return func(ctx context.Context) error { 48 return nil 49 } 50 } 51 52 func (sr *stepRun) getRunContext() *RunContext { 53 return sr.RunContext 54 } 55 56 func (sr *stepRun) getGithubContext(ctx context.Context) *model.GithubContext { 57 return sr.getRunContext().getGithubContext(ctx) 58 } 59 60 func (sr *stepRun) getStepModel() *model.Step { 61 return sr.Step 62 } 63 64 func (sr *stepRun) getEnv() *map[string]string { 65 return &sr.env 66 } 67 68 func (sr *stepRun) getIfExpression(_ context.Context, _ stepStage) string { 69 return sr.Step.If.Value 70 } 71 72 func (sr *stepRun) setupShellCommandExecutor() common.Executor { 73 return func(ctx context.Context) error { 74 scriptName, script, err := sr.setupShellCommand(ctx) 75 if err != nil { 76 return err 77 } 78 79 rc := sr.getRunContext() 80 return rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{ 81 Name: scriptName, 82 Mode: 0o755, 83 Body: script, 84 })(ctx) 85 } 86 } 87 88 func getScriptName(rc *RunContext, step *model.Step) string { 89 scriptName := step.ID 90 for rcs := rc; rcs.Parent != nil; rcs = rcs.Parent { 91 scriptName = fmt.Sprintf("%s-composite-%s", rcs.Parent.CurrentStep, scriptName) 92 } 93 return fmt.Sprintf("workflow/%s", scriptName) 94 } 95 96 // TODO: Currently we just ignore top level keys, BUT we should return proper error on them 97 // BUTx2 I leave this for when we rewrite act to use actionlint for workflow validation 98 // so we return proper errors before any execution or spawning containers 99 // it will error anyway with: 100 // OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "${{": executable file not found in $PATH: unknown 101 func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string, err error) { 102 logger := common.Logger(ctx) 103 sr.setupShell(ctx) 104 sr.setupWorkingDirectory(ctx) 105 106 step := sr.Step 107 108 script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run) 109 110 scCmd := step.ShellCommand() 111 112 name = getScriptName(sr.RunContext, step) 113 114 // Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L47-L64 115 // Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L19-L27 116 runPrepend := "" 117 runAppend := "" 118 switch step.Shell { 119 case "bash", "sh": 120 name += ".sh" 121 case "pwsh", "powershell": 122 name += ".ps1" 123 runPrepend = "$ErrorActionPreference = 'stop'" 124 runAppend = "if ((Test-Path -LiteralPath variable:/LASTEXITCODE)) { exit $LASTEXITCODE }" 125 case "cmd": 126 name += ".cmd" 127 runPrepend = "@echo off" 128 case "python": 129 name += ".py" 130 } 131 132 script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend) 133 134 if !strings.Contains(script, "::add-mask::") && !sr.RunContext.Config.InsecureSecrets { 135 logger.Debugf("Wrote command \n%s\n to '%s'", script, name) 136 } else { 137 logger.Debugf("Wrote add-mask command to '%s'", name) 138 } 139 140 rc := sr.getRunContext() 141 scriptPath := fmt.Sprintf("%s/%s", rc.JobContainer.GetActPath(), name) 142 sr.cmdline = strings.Replace(scCmd, `{0}`, scriptPath, 1) 143 sr.cmd, err = shellquote.Split(sr.cmdline) 144 145 return name, script, err 146 } 147 148 type localEnv struct { 149 env map[string]string 150 } 151 152 func (l *localEnv) Getenv(name string) string { 153 if runtime.GOOS == "windows" { 154 for k, v := range l.env { 155 if strings.EqualFold(name, k) { 156 return v 157 } 158 } 159 return "" 160 } 161 return l.env[name] 162 } 163 164 func (sr *stepRun) setupShell(ctx context.Context) { 165 rc := sr.RunContext 166 step := sr.Step 167 168 if step.Shell == "" { 169 step.Shell = rc.Run.Job().Defaults.Run.Shell 170 } 171 172 step.Shell = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, step.Shell) 173 174 if step.Shell == "" { 175 step.Shell = rc.Run.Workflow.Defaults.Run.Shell 176 } 177 178 if step.Shell == "" { 179 if _, ok := rc.JobContainer.(*container.HostEnvironment); ok { 180 shellWithFallback := []string{"bash", "sh"} 181 // Don't use bash on windows by default, if not using a docker container 182 if runtime.GOOS == "windows" { 183 shellWithFallback = []string{"pwsh", "powershell"} 184 } 185 step.Shell = shellWithFallback[0] 186 lenv := &localEnv{env: map[string]string{}} 187 for k, v := range sr.env { 188 lenv.env[k] = v 189 } 190 sr.getRunContext().ApplyExtraPath(ctx, &lenv.env) 191 _, err := lookpath.LookPath2(shellWithFallback[0], lenv) 192 if err != nil { 193 step.Shell = shellWithFallback[1] 194 } 195 } else if containerImage := rc.containerImage(ctx); containerImage != "" { 196 // Currently only linux containers are supported, use sh by default like actions/runner 197 step.Shell = "sh" 198 } 199 } 200 } 201 202 func (sr *stepRun) setupWorkingDirectory(ctx context.Context) { 203 rc := sr.RunContext 204 step := sr.Step 205 workingdirectory := "" 206 207 if step.WorkingDirectory == "" { 208 workingdirectory = rc.Run.Job().Defaults.Run.WorkingDirectory 209 } else { 210 workingdirectory = step.WorkingDirectory 211 } 212 213 // jobs can receive context values, so we interpolate 214 workingdirectory = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, workingdirectory) 215 216 // but top level keys in workflow file like `defaults` or `env` can't 217 if workingdirectory == "" { 218 workingdirectory = rc.Run.Workflow.Defaults.Run.WorkingDirectory 219 } 220 sr.WorkingDirectory = workingdirectory 221 }