f2c15074ac
This is a solution to issue #416 where environment variables created or changed in the previous step are not usable in the next step because the rc.ExprEval is from the beginning of the previous step. This change refactors setupEnv so that before interpolating the environment variables a NewExpressionEvaluator is created. Fixes: 416
430 lines
12 KiB
Go
430 lines
12 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
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
|
|
}
|
|
|
|
func (sc *StepContext) execJobContainer() common.Executor {
|
|
return func(ctx context.Context) error {
|
|
return sc.RunContext.execJobContainer(sc.Cmd, sc.Env)(ctx)
|
|
}
|
|
}
|
|
|
|
// 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, ""),
|
|
sc.runAction(actionDir, ""),
|
|
)
|
|
case model.StepTypeUsesActionRemote:
|
|
remoteAction := newRemoteAction(step.Uses)
|
|
if remoteAction.IsCheckout() && rc.getGithubContext().isLocalCheckout(step) {
|
|
return func(ctx context.Context) error {
|
|
common.Logger(ctx).Debugf("Skipping actions/checkout")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-"))
|
|
return common.NewPipelineExecutor(
|
|
common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
|
URL: remoteAction.CloneURL(),
|
|
Ref: remoteAction.Ref,
|
|
Dir: actionDir,
|
|
}),
|
|
sc.setupAction(actionDir, remoteAction.Path),
|
|
sc.runAction(actionDir, remoteAction.Path),
|
|
)
|
|
}
|
|
|
|
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()
|
|
step := sc.Step
|
|
|
|
var env map[string]string
|
|
c := job.Container()
|
|
if c != nil {
|
|
env = mergeMaps(rc.GetEnv(), c.Env, step.GetEnv())
|
|
} else {
|
|
env = mergeMaps(rc.GetEnv(), step.GetEnv())
|
|
}
|
|
|
|
if (rc.ExtraPath != nil) && (len(rc.ExtraPath) > 0) {
|
|
s := append(rc.ExtraPath, `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`)
|
|
env["PATH"] = strings.Join(s, `:`)
|
|
}
|
|
|
|
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) setupEnv(ctx context.Context) (ExpressionEvaluator, error) {
|
|
rc := sc.RunContext
|
|
sc.Env = sc.mergeEnv()
|
|
if sc.Env != nil {
|
|
err := rc.JobContainer.UpdateFromGithubEnv(&sc.Env)(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
evaluator := sc.NewExpressionEvaluator()
|
|
sc.interpolateEnv(evaluator)
|
|
|
|
log.Debugf("setupEnv: %v", sc.Env)
|
|
return evaluator, nil
|
|
}
|
|
|
|
|
|
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
|
|
}
|
|
if step.WorkingDirectory != "" {
|
|
_, err = script.WriteString(fmt.Sprintf("cd %s\n", step.WorkingDirectory))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
run := rc.ExprEval.Interpolate(step.Run)
|
|
|
|
if _, err = script.WriteString(run); err != nil {
|
|
return err
|
|
}
|
|
scriptName := fmt.Sprintf("workflow/%s", step.ID)
|
|
log.Debugf("Wrote command '%s' to '%s'", run, scriptName)
|
|
containerPath := fmt.Sprintf("/github/%s", scriptName)
|
|
|
|
if step.Shell == "" {
|
|
step.Shell = rc.Run.Job().Defaults.Run.Shell
|
|
}
|
|
if step.Shell == "" {
|
|
step.Shell = rc.Run.Workflow.Defaults.Run.Shell
|
|
}
|
|
sc.Cmd = strings.Fields(strings.Replace(step.ShellCommand(), "{0}", containerPath, 1))
|
|
return rc.JobContainer.Copy("/github/", &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)
|
|
}
|
|
|
|
bindModifiers := ""
|
|
if runtime.GOOS == "darwin" {
|
|
bindModifiers = ":delegated"
|
|
}
|
|
|
|
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 := []string{
|
|
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
|
}
|
|
if rc.Config.BindWorkdir {
|
|
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers))
|
|
}
|
|
|
|
stepContainer := container.NewContainer(&container.NewContainerInput{
|
|
Cmd: cmd,
|
|
Entrypoint: entrypoint,
|
|
WorkingDir: "/github/workspace",
|
|
Image: image,
|
|
Name: createContainerName(rc.jobContainerName(), step.ID),
|
|
Env: envList,
|
|
Mounts: map[string]string{
|
|
rc.jobContainerName(): "/github",
|
|
"act-toolcache": "/toolcache",
|
|
"act-actions": "/actions",
|
|
},
|
|
NetworkMode: fmt.Sprintf("container:%s", rc.jobContainerName()),
|
|
Binds: binds,
|
|
Stdout: logWriter,
|
|
Stderr: logWriter,
|
|
Privileged: rc.Config.Privileged,
|
|
})
|
|
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 := strings.Fields(step.With["args"])
|
|
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(),
|
|
stepContainer.Start(true),
|
|
).Finally(
|
|
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
|
|
)(ctx)
|
|
}
|
|
}
|
|
|
|
func (sc *StepContext) setupAction(actionDir string, actionPath string) common.Executor {
|
|
return func(ctx context.Context) error {
|
|
f, err := os.Open(filepath.Join(actionDir, actionPath, "action.yml"))
|
|
if os.IsNotExist(err) {
|
|
f, err = os.Open(filepath.Join(actionDir, actionPath, "action.yaml"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
sc.Action, err = model.ReadAction(f)
|
|
log.Debugf("Read action %v from '%s'", sc.Action, f.Name())
|
|
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.StepTypeUsesActionLocal {
|
|
actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
|
|
containerActionDir = "/github/workspace"
|
|
} else if step.Type() == model.StepTypeUsesActionRemote {
|
|
actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
|
|
containerActionDir = "/actions"
|
|
}
|
|
|
|
if actionName == "" {
|
|
actionName = filepath.Base(actionDir)
|
|
if runtime.GOOS == "windows" {
|
|
actionName = strings.ReplaceAll(actionName, "\\", "/")
|
|
}
|
|
}
|
|
return actionName, containerActionDir
|
|
}
|
|
|
|
func (sc *StepContext) runAction(actionDir string, actionPath string) 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)
|
|
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)
|
|
}
|
|
}
|
|
|
|
actionName, containerActionDir := sc.getContainerActionPaths(step, actionDir, rc)
|
|
|
|
sc.Env = mergeMaps(sc.Env, action.Runs.Env)
|
|
|
|
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)
|
|
|
|
switch action.Runs.Using {
|
|
case model.ActionRunsUsingNode12:
|
|
if step.Type() == model.StepTypeUsesActionRemote {
|
|
err := removeGitIgnore(actionDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = rc.JobContainer.CopyDir(containerActionDir+"/", actionDir)(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
containerArgs := []string{"node", path.Join(containerActionDir, actionName, actionPath, action.Runs.Main)}
|
|
log.Debugf("executing remote job container: %s", containerArgs)
|
|
return rc.execJobContainer(containerArgs, sc.Env)(ctx)
|
|
case model.ActionRunsUsingDocker:
|
|
var prepImage common.Executor
|
|
var image string
|
|
if strings.HasPrefix(action.Runs.Image, "docker://") {
|
|
image = strings.TrimPrefix(action.Runs.Image, "docker://")
|
|
} else {
|
|
image = fmt.Sprintf("%s:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
|
|
image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
|
|
image = strings.ToLower(image)
|
|
contextDir := filepath.Join(actionDir, actionPath, action.Runs.Main)
|
|
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
|
ContextDir: contextDir,
|
|
ImageTag: image,
|
|
})
|
|
}
|
|
|
|
cmd := strings.Fields(step.With["args"])
|
|
if len(cmd) == 0 {
|
|
cmd = action.Runs.Args
|
|
}
|
|
entrypoint := strings.Fields(step.With["entrypoint"])
|
|
if len(entrypoint) == 0 {
|
|
entrypoint = action.Runs.Entrypoint
|
|
}
|
|
stepContainer := sc.newStepContainer(ctx, image, cmd, entrypoint)
|
|
return common.NewPipelineExecutor(
|
|
prepImage,
|
|
stepContainer.Pull(rc.Config.ForcePull),
|
|
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
|
|
stepContainer.Create(),
|
|
stepContainer.Start(true),
|
|
).Finally(
|
|
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
|
|
)(ctx)
|
|
default:
|
|
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
|
model.ActionRunsUsingDocker,
|
|
model.ActionRunsUsingNode12,
|
|
}, action.Runs.Using))
|
|
}
|
|
}
|
|
}
|
|
|
|
type remoteAction struct {
|
|
Org string
|
|
Repo string
|
|
Path string
|
|
Ref string
|
|
}
|
|
|
|
func (ra *remoteAction) CloneURL() string {
|
|
return fmt.Sprintf("https://github.com/%s/%s", 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 {
|
|
r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`)
|
|
matches := r.FindStringSubmatch(action)
|
|
|
|
ra := new(remoteAction)
|
|
ra.Org = matches[1]
|
|
ra.Repo = matches[2]
|
|
ra.Path = ""
|
|
ra.Ref = "master"
|
|
if len(matches) >= 5 {
|
|
ra.Path = matches[4]
|
|
}
|
|
if len(matches) >= 7 {
|
|
ra.Ref = matches[6]
|
|
}
|
|
return ra
|
|
}
|
|
|
|
// 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
|
|
}
|