mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-09-05 18:40:59 +00:00
The `setupShell` function would update the shell stored on the `Step` object, setting it to either a default value from the job, an expression evaluated in the context of the job, a default from the workflow, or finally falling back to bash or powershell defaults. Typically this would be fine -- although it would trigger the data race detector because the `Step` is a shared object between multiple concurrent matrix evaluations for the job. In the *really quite unlikely* case that the `shell` field on a step or job referenced a matrix variable, this data race would actually trigger the shared step's `Shell` value to end up as "whichever one was evaluated last", causing the wrong shell to be used. The new `matrix-shell` test triggers this behavior, and fails without the associated code fix. As a fix, the `Shell` field in `Step` is never mutated; instead only the value on non-shared `stepRun` instance is updated from `setupShellCommand`. `Shell` was renamed to `RawShell` as part of verifying all references were updated and it seemed to make sense to keep that name since it is a pre-evaluator value. ``` ================== WARNING: DATA RACE Write at 0x00c00013e9b0 by goroutine 1470: code.forgejo.org/forgejo/runner/v9/act/runner.(*stepRun).setupShell() /.../forgejo-runner/act/runner/step_run.go:210 +0x8f2 code.forgejo.org/forgejo/runner/v9/act/common/git.FindGitRevision() /.../forgejo-runner/act/common/git/git.go:58 +0xc4 code.forgejo.org/forgejo/runner/v9/act/model.(*GithubContext).SetSha() /.../forgejo-runner/act/model/github_context.go:161 +0x6b5 code.forgejo.org/forgejo/runner/v9/act/runner.(*RunContext).getGithubContext() /.../forgejo-runner/act/runner/run_context.go:1228 +0x26ca ... Previous write at 0x00c00013e9b0 by goroutine 1469: code.forgejo.org/forgejo/runner/v9/act/runner.(*stepRun).setupShell() /.../forgejo-runner/act/runner/step_run.go:210 +0x8f2 code.forgejo.org/forgejo/runner/v9/act/common/git.FindGitRevision() /.../forgejo-runner/act/common/git/git.go:58 +0xc4 code.forgejo.org/forgejo/runner/v9/act/model.(*GithubContext).SetSha() /.../forgejo-runner/act/model/github_context.go:161 +0x6b5 code.forgejo.org/forgejo/runner/v9/act/runner.(*RunContext).getGithubContext() /.../forgejo-runner/act/runner/run_context.go:1228 +0x26ca ... ================== ``` <!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - bug fixes - [PR](https://code.forgejo.org/forgejo/runner/pulls/865): <!--number 865 --><!--line 0 --><!--description Zml4OiBkYXRhIHJhY2UgY29uZGl0aW9uIGNhdXNpbmcgaW5jb3JyZWN0IGBzaGVsbGAgb24gYSB0YXNrIHN0ZXAgaWYgaXQgcmVmZXJlbmNlZCBhIG1hdHJpeCB2YXJpYWJsZQ==-->fix: data race condition causing incorrect `shell` on a task step if it referenced a matrix variable<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/865 Reviewed-by: earl-warren <earl-warren@noreply.code.forgejo.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
256 lines
6.9 KiB
Go
256 lines
6.9 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/kballard/go-shellquote"
|
|
|
|
"code.forgejo.org/forgejo/runner/v9/act/common"
|
|
"code.forgejo.org/forgejo/runner/v9/act/container"
|
|
"code.forgejo.org/forgejo/runner/v9/act/lookpath"
|
|
"code.forgejo.org/forgejo/runner/v9/act/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)
|
|
}
|
|
|
|
func shellCommand(shell string) string {
|
|
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
|
|
switch shell {
|
|
case "", "bash":
|
|
return "bash --noprofile --norc -e -o pipefail {0}"
|
|
case "pwsh":
|
|
return "pwsh -command . '{0}'"
|
|
case "python":
|
|
return "python {0}"
|
|
case "sh":
|
|
return "sh -e {0}"
|
|
case "cmd":
|
|
return "cmd /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
|
|
case "powershell":
|
|
return "powershell -command . '{0}'"
|
|
case "node":
|
|
return "node {0}"
|
|
default:
|
|
return shell
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
shell := sr.interpretShell(ctx)
|
|
sr.setupWorkingDirectory(ctx)
|
|
|
|
step := sr.Step
|
|
|
|
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
|
|
|
shellCommand := shellCommand(shell)
|
|
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 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"
|
|
case "node":
|
|
name += ".js"
|
|
}
|
|
|
|
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(shellCommand, `{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) interpretShell(ctx context.Context) string {
|
|
rc := sr.RunContext
|
|
shell := sr.Step.RawShell
|
|
|
|
if shell == "" {
|
|
shell = rc.Run.Job().Defaults.Run.Shell
|
|
}
|
|
|
|
shell = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, shell)
|
|
|
|
if shell == "" {
|
|
shell = rc.Run.Workflow.Defaults.Run.Shell
|
|
}
|
|
|
|
if 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"}
|
|
}
|
|
shell = shellWithFallback[0]
|
|
lenv := &localEnv{env: map[string]string{}}
|
|
maps.Copy(lenv.env, sr.env)
|
|
sr.getRunContext().ApplyExtraPath(ctx, &lenv.env)
|
|
_, err := lookpath.LookPath2(shellWithFallback[0], lenv)
|
|
if err != nil {
|
|
shell = shellWithFallback[1]
|
|
}
|
|
} else {
|
|
shellFallback := `
|
|
if command -v bash >/dev/null; then
|
|
echo -n bash
|
|
else
|
|
echo -n sh
|
|
fi
|
|
`
|
|
stdout, _, err := rc.sh(ctx, shellFallback)
|
|
if err != nil {
|
|
common.Logger(ctx).Error("fail to run %q: %v", shellFallback, err)
|
|
} else {
|
|
shell = stdout
|
|
}
|
|
}
|
|
}
|
|
|
|
return shell
|
|
}
|
|
|
|
func (sr *stepRun) setupWorkingDirectory(ctx context.Context) {
|
|
rc := sr.RunContext
|
|
step := sr.Step
|
|
var workingdirectory string
|
|
|
|
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
|
|
}
|