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  }