act/pkg/runner/step_context.go
Markus Wolf 1891c72ab1
fix: continue jobs + steps after failure (#840)
* fix: continue jobs + steps after failure

To allow proper if expression handling on jobs and steps (like always,
success, failure, ...) we need to continue running all executors in
the prepared chain.
To keep the error handling intact we add an occurred error to the
go context and handle it later in the pipeline/chain.

Also we add the job result to the needs context to give expressions
access to it.
The needs object, failure and success functions are split between
run context (on jobs) and step context.

Closes #442

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* style: correct linter warnings

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* fix: job if value defaults to success()

As described in the documentation, a default value of "success()" is
applied when no "if" value is present on the job.

https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions

Co-authored-by: Markus Wolf <mail@markus-wolf.de>

* fix: check job needs recursively

Ensure job result includes results of previous jobs

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* test: add runner test for job status check functions

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* test: add unit tests for run context if evaluation

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* refactor: move if expression evaluation

Move if expression evaluation into own function (step context) to
better support unit testing.

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>

* test: add unit tests for step context if evaluation

Co-authored-by: Markus Wolf <markus.wolf@new-work.se>

* fix: handle job error more resilient

The job error is not stored in a context map instead of a context
added value.
Since context values are immutable an added value requires to keep
the new context in all cases. This is fragile since it might slip
unnoticed to other parts of the code.

Storing the error of a job in the context map will make it more stable,
since the map is always there and the context of the pipeline is stable
for the whole run.

* feat: steps should use a default if expression of success()

* test: add integration test for if-expressions

* chore: disable editorconfig-checker for yaml multiline string

Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de>
Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
2021-12-08 20:57:42 +00:00

757 lines
23 KiB
Go

package runner
import (
"archive/tar"
"context"
"embed"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/kballard/go-shellquote"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model"
)
// StepContext contains info about current job
type StepContext struct {
RunContext *RunContext
Step *model.Step
Env map[string]string
Cmd []string
Action *model.Action
Needs *model.Job
}
func (sc *StepContext) execJobContainer() common.Executor {
return func(ctx context.Context) error {
return sc.RunContext.execJobContainer(sc.Cmd, sc.Env, "", sc.Step.WorkingDirectory)(ctx)
}
}
type formatError string
func (e formatError) Error() string {
return fmt.Sprintf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format.", string(e))
}
// Executor for a step context
func (sc *StepContext) Executor() common.Executor {
rc := sc.RunContext
step := sc.Step
switch step.Type() {
case model.StepTypeRun:
return common.NewPipelineExecutor(
sc.setupShellCommand(),
sc.execJobContainer(),
)
case model.StepTypeUsesDockerURL:
return common.NewPipelineExecutor(
sc.runUsesContainer(),
)
case model.StepTypeUsesActionLocal:
actionDir := filepath.Join(rc.Config.Workdir, step.Uses)
return common.NewPipelineExecutor(
sc.setupAction(actionDir, "", true),
sc.runAction(actionDir, "", true),
)
case model.StepTypeUsesActionRemote:
remoteAction := newRemoteAction(step.Uses)
if remoteAction == nil {
return common.NewErrorExecutor(formatError(step.Uses))
}
remoteAction.URL = rc.Config.GitHubInstance
github := rc.getGithubContext()
if remoteAction.IsCheckout() && github.isLocalCheckout(step) {
return func(ctx context.Context) error {
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
return nil
}
}
actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-"))
gitClone := common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
URL: remoteAction.CloneURL(),
Ref: remoteAction.Ref,
Dir: actionDir,
Token: github.Token,
})
var ntErr common.Executor
if err := gitClone(context.TODO()); err != nil {
if err.Error() == "short SHA references are not supported" {
err = errors.Cause(err)
return common.NewErrorExecutor(fmt.Errorf("Unable to resolve action `%s`, the provided ref `%s` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `%s` instead", step.Uses, remoteAction.Ref, err.Error()))
} else if err.Error() != "some refs were not updated" {
return common.NewErrorExecutor(err)
} else {
ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err)
}
}
return common.NewPipelineExecutor(
ntErr,
sc.setupAction(actionDir, remoteAction.Path, false),
sc.runAction(actionDir, remoteAction.Path, false),
)
case model.StepTypeInvalid:
return common.NewErrorExecutor(fmt.Errorf("Invalid run/uses syntax for job:%s step:%+v", rc.Run, step))
}
return common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step))
}
func (sc *StepContext) mergeEnv() map[string]string {
rc := sc.RunContext
job := rc.Run.Job()
var env map[string]string
c := job.Container()
if c != nil {
env = mergeMaps(rc.GetEnv(), c.Env)
} else {
env = rc.GetEnv()
}
if env["PATH"] == "" {
env["PATH"] = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
}
if rc.ExtraPath != nil && len(rc.ExtraPath) > 0 {
p := env["PATH"]
env["PATH"] = strings.Join(rc.ExtraPath, `:`)
env["PATH"] += `:` + p
}
sc.Env = rc.withGithubEnv(env)
return env
}
func (sc *StepContext) interpolateEnv(exprEval ExpressionEvaluator) {
for k, v := range sc.Env {
sc.Env[k] = exprEval.Interpolate(v)
}
}
func (sc *StepContext) isEnabled(ctx context.Context) (bool, error) {
runStep, err := EvalBool(sc.NewExpressionEvaluator(), sc.Step.If.Value)
if err != nil {
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", sc.Step)
exprEval, err := sc.setupEnv(ctx)
if err != nil {
return false, err
}
sc.RunContext.ExprEval = exprEval
return false, err
}
return runStep, nil
}
func (sc *StepContext) setupEnv(ctx context.Context) (ExpressionEvaluator, error) {
rc := sc.RunContext
sc.Env = sc.mergeEnv()
if sc.Env != nil {
err := rc.JobContainer.UpdateFromImageEnv(&sc.Env)(ctx)
if err != nil {
return nil, err
}
err = rc.JobContainer.UpdateFromEnv(sc.Env["GITHUB_ENV"], &sc.Env)(ctx)
if err != nil {
return nil, err
}
err = rc.JobContainer.UpdateFromPath(&sc.Env)(ctx)
if err != nil {
return nil, err
}
}
sc.Env = mergeMaps(sc.Env, sc.Step.GetEnv()) // step env should not be overwritten
evaluator := sc.NewExpressionEvaluator()
sc.interpolateEnv(evaluator)
common.Logger(ctx).Debugf("setupEnv => %v", sc.Env)
return evaluator, nil
}
// nolint:gocyclo
func (sc *StepContext) setupShellCommand() common.Executor {
rc := sc.RunContext
step := sc.Step
return func(ctx context.Context) error {
var script strings.Builder
var err error
if step.WorkingDirectory == "" {
step.WorkingDirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
}
if step.WorkingDirectory == "" {
step.WorkingDirectory = rc.Run.Workflow.Defaults.Run.WorkingDirectory
}
step.WorkingDirectory = rc.ExprEval.Interpolate(step.WorkingDirectory)
run := rc.ExprEval.Interpolate(step.Run)
step.Shell = rc.ExprEval.Interpolate(step.Shell)
if _, err = script.WriteString(run); err != nil {
return err
}
scriptName := fmt.Sprintf("workflow/%s", step.ID)
// 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 := ""
scriptExt := ""
switch step.Shell {
case "bash", "sh":
scriptExt = ".sh"
case "pwsh", "powershell":
scriptExt = ".ps1"
runPrepend = "$ErrorActionPreference = 'stop'"
runAppend = "if ((Test-Path -LiteralPath variable:/LASTEXITCODE)) { exit $LASTEXITCODE }"
case "cmd":
scriptExt = ".cmd"
runPrepend = "@echo off"
case "python":
scriptExt = ".py"
}
scriptName += scriptExt
run = runPrepend + "\n" + run + "\n" + runAppend
log.Debugf("Wrote command '%s' to '%s'", run, scriptName)
scriptPath := fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), scriptName)
if step.Shell == "" {
step.Shell = rc.Run.Job().Defaults.Run.Shell
}
if step.Shell == "" {
step.Shell = rc.Run.Workflow.Defaults.Run.Shell
}
if rc.Run.Job().Container() != nil {
if rc.Run.Job().Container().Image != "" && step.Shell == "" {
step.Shell = "sh"
}
}
scCmd := step.ShellCommand()
var finalCMD []string
if step.Shell == "pwsh" || step.Shell == "powershell" {
finalCMD = strings.SplitN(scCmd, " ", 3)
} else {
finalCMD = strings.Fields(scCmd)
}
for k, v := range finalCMD {
if strings.Contains(v, `{0}`) {
finalCMD[k] = strings.Replace(v, `{0}`, scriptPath, 1)
}
}
sc.Cmd = finalCMD
return rc.JobContainer.Copy(rc.Config.ContainerWorkdir(), &container.FileEntry{
Name: scriptName,
Mode: 0755,
Body: script.String(),
})(ctx)
}
}
func (sc *StepContext) newStepContainer(ctx context.Context, image string, cmd []string, entrypoint []string) container.Container {
rc := sc.RunContext
step := sc.Step
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
envList := make([]string, 0)
for k, v := range sc.Env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
}
stepEE := sc.NewExpressionEvaluator()
for i, v := range cmd {
cmd[i] = stepEE.Interpolate(v)
}
for i, v := range entrypoint {
entrypoint[i] = stepEE.Interpolate(v)
}
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
binds, mounts := rc.GetBindsAndMounts()
stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
WorkingDir: rc.Config.ContainerWorkdir(),
Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
Name: createContainerName(rc.jobContainerName(), step.ID),
Env: envList,
Mounts: mounts,
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
Binds: binds,
Stdout: logWriter,
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
})
return stepContainer
}
func (sc *StepContext) runUsesContainer() common.Executor {
rc := sc.RunContext
step := sc.Step
return func(ctx context.Context) error {
image := strings.TrimPrefix(step.Uses, "docker://")
cmd, err := shellquote.Split(sc.RunContext.NewExpressionEvaluator().Interpolate(step.With["args"]))
if err != nil {
return err
}
entrypoint := strings.Fields(step.With["entrypoint"])
stepContainer := sc.newStepContainer(ctx, image, cmd, entrypoint)
return common.NewPipelineExecutor(
stepContainer.Pull(rc.Config.ForcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true),
).Finally(
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
).Finally(stepContainer.Close())(ctx)
}
}
//go:embed res/trampoline.js
var trampoline embed.FS
func (sc *StepContext) setupAction(actionDir string, actionPath string, localAction bool) common.Executor {
return func(ctx context.Context) error {
var readFile func(filename string) (io.Reader, io.Closer, error)
if localAction {
_, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, actionPath), sc.RunContext)
readFile = func(filename string) (io.Reader, io.Closer, error) {
tars, err := sc.RunContext.JobContainer.GetContainerArchive(ctx, path.Join(cpath, filename))
if err != nil {
return nil, nil, os.ErrNotExist
}
treader := tar.NewReader(tars)
if _, err := treader.Next(); err != nil {
return nil, nil, os.ErrNotExist
}
return treader, tars, nil
}
} else {
readFile = func(filename string) (io.Reader, io.Closer, error) {
f, err := os.Open(filepath.Join(actionDir, actionPath, filename))
return f, f, err
}
}
reader, closer, err := readFile("action.yml")
if os.IsNotExist(err) {
reader, closer, err = readFile("action.yaml")
if err != nil {
if _, closer, err2 := readFile("Dockerfile"); err2 == nil {
closer.Close()
sc.Action = &model.Action{
Name: "(Synthetic)",
Runs: model.ActionRuns{
Using: "docker",
Image: "Dockerfile",
},
}
log.Debugf("Using synthetic action %v for Dockerfile", sc.Action)
return nil
}
if sc.Step.With != nil {
if val, ok := sc.Step.With["args"]; ok {
var b []byte
if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
return err
}
err2 := ioutil.WriteFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400)
if err2 != nil {
return err
}
sc.Action = &model.Action{
Name: "(Synthetic)",
Inputs: map[string]model.Input{
"cwd": {
Description: "(Actual working directory)",
Required: false,
Default: filepath.Join(actionDir, actionPath),
},
"command": {
Description: "(Actual program)",
Required: false,
Default: val,
},
},
Runs: model.ActionRuns{
Using: "node12",
Main: "trampoline.js",
},
}
log.Debugf("Using synthetic action %v", sc.Action)
return nil
}
}
return err
}
} else if err != nil {
return err
}
defer closer.Close()
sc.Action, err = model.ReadAction(reader)
log.Debugf("Read action %v from '%s'", sc.Action, "Unknown")
return err
}
}
func getOsSafeRelativePath(s, prefix string) string {
actionName := strings.TrimPrefix(s, prefix)
if runtime.GOOS == "windows" {
actionName = strings.ReplaceAll(actionName, "\\", "/")
}
actionName = strings.TrimPrefix(actionName, "/")
return actionName
}
func (sc *StepContext) getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) {
actionName := ""
containerActionDir := "."
if step.Type() != model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
containerActionDir = rc.Config.ContainerWorkdir() + "/" + actionName
actionName = "./" + actionName
} else if step.Type() == model.StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
containerActionDir = ActPath + "/actions/" + actionName
}
if actionName == "" {
actionName = filepath.Base(actionDir)
if runtime.GOOS == "windows" {
actionName = strings.ReplaceAll(actionName, "\\", "/")
}
}
return actionName, containerActionDir
}
// nolint: gocyclo
func (sc *StepContext) runAction(actionDir string, actionPath string, localAction bool) common.Executor {
rc := sc.RunContext
step := sc.Step
return func(ctx context.Context) error {
action := sc.Action
log.Debugf("About to run action %v", action)
sc.populateEnvsFromInput(action, rc)
actionLocation := ""
if actionPath != "" {
actionLocation = path.Join(actionDir, actionPath)
} else {
actionLocation = actionDir
}
actionName, containerActionDir := sc.getContainerActionPaths(step, actionLocation, rc)
sc.Env = mergeMaps(sc.Env, action.Runs.Env)
ee := sc.NewExpressionEvaluator()
for k, v := range sc.Env {
sc.Env[k] = ee.Interpolate(v)
}
log.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", step.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir)
maybeCopyToActionDir := func() error {
sc.Env["GITHUB_ACTION_PATH"] = containerActionDir
if step.Type() != model.StepTypeUsesActionRemote {
return nil
}
if err := removeGitIgnore(actionDir); err != nil {
return err
}
var containerActionDirCopy string
containerActionDirCopy = strings.TrimSuffix(containerActionDir, actionPath)
log.Debug(containerActionDirCopy)
if !strings.HasSuffix(containerActionDirCopy, `/`) {
containerActionDirCopy += `/`
}
return rc.JobContainer.CopyDir(containerActionDirCopy, actionDir+"/", rc.Config.UseGitIgnore)(ctx)
}
switch action.Runs.Using {
case model.ActionRunsUsingNode12:
if err := maybeCopyToActionDir(); err != nil {
return err
}
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
log.Debugf("executing remote job container: %s", containerArgs)
return rc.execJobContainer(containerArgs, sc.Env, "", "")(ctx)
case model.ActionRunsUsingDocker:
return sc.execAsDocker(ctx, action, actionName, containerActionDir, actionLocation, rc, step, localAction)
case model.ActionRunsUsingComposite:
return sc.execAsComposite(ctx, step, actionDir, rc, containerActionDir, actionName, actionPath, action, maybeCopyToActionDir)
default:
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
model.ActionRunsUsingDocker,
model.ActionRunsUsingNode12,
model.ActionRunsUsingComposite,
}, action.Runs.Using))
}
}
}
// TODO: break out parts of function to reduce complexicity
// nolint:gocyclo
func (sc *StepContext) execAsDocker(ctx context.Context, action *model.Action, actionName string, containerLocation string, actionLocation string, rc *RunContext, step *model.Step, localAction bool) error {
var prepImage common.Executor
var image string
if strings.HasPrefix(action.Runs.Image, "docker://") {
image = strings.TrimPrefix(action.Runs.Image, "docker://")
} else {
// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
image = strings.ToLower(image)
basedir := actionLocation
if localAction {
basedir = containerLocation
}
contextDir := filepath.Join(basedir, action.Runs.Main)
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
if err != nil {
return err
}
correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
if err != nil {
return err
}
if anyArchExists && !correctArchExists {
wasRemoved, err := container.RemoveImage(ctx, image, true, true)
if err != nil {
return err
}
if !wasRemoved {
return fmt.Errorf("failed to remove image '%s'", image)
}
}
if !correctArchExists || rc.Config.ForceRebuild {
log.Debugf("image '%s' for architecture '%s' will be built from context '%s", image, rc.Config.ContainerArchitecture, contextDir)
var actionContainer container.Container
if localAction {
actionContainer = sc.RunContext.JobContainer
}
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir,
ImageTag: image,
Container: actionContainer,
Platform: rc.Config.ContainerArchitecture,
})
} else {
log.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
}
}
cmd, err := shellquote.Split(step.With["args"])
if err != nil {
return err
}
if len(cmd) == 0 {
cmd = action.Runs.Args
}
entrypoint := strings.Fields(step.With["entrypoint"])
if len(entrypoint) == 0 {
if action.Runs.Entrypoint != "" {
entrypoint, err = shellquote.Split(action.Runs.Entrypoint)
if err != nil {
return err
}
} else {
entrypoint = nil
}
}
stepContainer := sc.newStepContainer(ctx, image, cmd, entrypoint)
return common.NewPipelineExecutor(
prepImage,
stepContainer.Pull(rc.Config.ForcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true),
).Finally(
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
).Finally(stepContainer.Close())(ctx)
}
func (sc *StepContext) execAsComposite(ctx context.Context, step *model.Step, _ string, rc *RunContext, containerActionDir string, actionName string, _ string, action *model.Action, maybeCopyToActionDir func() error) error {
err := maybeCopyToActionDir()
if err != nil {
return err
}
for outputName, output := range action.Outputs {
re := regexp.MustCompile(`\${{ steps\.([a-zA-Z_][a-zA-Z0-9_-]+)\.outputs\.([a-zA-Z_][a-zA-Z0-9_-]+) }}`)
matches := re.FindStringSubmatch(output.Value)
if len(matches) > 2 {
if sc.RunContext.OutputMappings == nil {
sc.RunContext.OutputMappings = make(map[MappableOutput]MappableOutput)
}
k := MappableOutput{StepID: matches[1], OutputName: matches[2]}
v := MappableOutput{StepID: step.ID, OutputName: outputName}
sc.RunContext.OutputMappings[k] = v
}
}
executors := make([]common.Executor, 0, len(action.Runs.Steps))
stepID := 0
for _, compositeStep := range action.Runs.Steps {
stepClone := compositeStep
// Take a copy of the run context structure (rc is a pointer)
// Then take the address of the new structure
rcCloneStr := *rc
rcClone := &rcCloneStr
if stepClone.ID == "" {
stepClone.ID = fmt.Sprintf("composite-%d", stepID)
stepID++
}
rcClone.CurrentStep = stepClone.ID
if err := compositeStep.Validate(); err != nil {
return err
}
// Setup the outputs for the composite steps
if _, ok := rcClone.StepResults[stepClone.ID]; !ok {
rcClone.StepResults[stepClone.ID] = &stepResult{
Conclusion: stepStatusSuccess,
Outputs: make(map[string]string),
}
}
env := stepClone.Environment()
stepContext := StepContext{
RunContext: rcClone,
Step: step,
Env: mergeMaps(sc.Env, env),
Action: action,
}
// Required to set github.action_path
if rcClone.Config.Env == nil {
// Workaround to get test working
rcClone.Config.Env = make(map[string]string)
}
rcClone.Config.Env["GITHUB_ACTION_PATH"] = sc.Env["GITHUB_ACTION_PATH"]
ev := stepContext.NewExpressionEvaluator()
// Required to interpolate inputs and github.action_path into the env map
stepContext.interpolateEnv(ev)
// Required to interpolate inputs, env and github.action_path into run steps
ev = stepContext.NewExpressionEvaluator()
stepClone.Run = ev.Interpolate(stepClone.Run)
stepClone.Shell = ev.Interpolate(stepClone.Shell)
stepClone.WorkingDirectory = ev.Interpolate(stepClone.WorkingDirectory)
stepContext.Step = &stepClone
executors = append(executors, stepContext.Executor())
}
return common.NewPipelineExecutor(executors...)(ctx)
}
func (sc *StepContext) populateEnvsFromInput(action *model.Action, rc *RunContext) {
for inputID, input := range action.Inputs {
envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
envKey = fmt.Sprintf("INPUT_%s", envKey)
if _, ok := sc.Env[envKey]; !ok {
sc.Env[envKey] = rc.ExprEval.Interpolate(input.Default)
}
}
}
type remoteAction struct {
URL string
Org string
Repo string
Path string
Ref string
}
func (ra *remoteAction) CloneURL() string {
return fmt.Sprintf("https://%s/%s/%s", ra.URL, ra.Org, ra.Repo)
}
func (ra *remoteAction) IsCheckout() bool {
if ra.Org == "actions" && ra.Repo == "checkout" {
return true
}
return false
}
func newRemoteAction(action string) *remoteAction {
// GitHub's document[^] describes:
// > We strongly recommend that you include the version of
// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
// Actually, the workflow stops if there is the uses directive that hasn't @ref.
// [^]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`)
matches := r.FindStringSubmatch(action)
if len(matches) < 7 || matches[6] == "" {
return nil
}
return &remoteAction{
Org: matches[1],
Repo: matches[2],
Path: matches[4],
Ref: matches[6],
URL: "github.com",
}
}
// https://github.com/nektos/act/issues/228#issuecomment-629709055
// files in .gitignore are not copied in a Docker container
// this causes issues with actions that ignore other important resources
// such as `node_modules` for example
func removeGitIgnore(directory string) error {
gitIgnorePath := path.Join(directory, ".gitignore")
if _, err := os.Stat(gitIgnorePath); err == nil {
// .gitignore exists
log.Debugf("Removing %s before docker cp", gitIgnorePath)
err := os.Remove(gitIgnorePath)
if err != nil {
return err
}
}
return nil
}