feat: split job steps into its own files/structs (#1004)
* refactor: split step_context into separate files This commit moves functions from the step_context.go file into different files, but does otherwise not change anything. This is done to make it easier to review the changes made to these functions in the next commit, where we introduce a step factory to facilitate better unit testing of steps. Co-authored-by: Marcus Noll <marcus.noll@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Robert Kowalski <robert.kowalski@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Jonas Holland <jonas.holland@new-work.se> * refactor: introduce step factory and make steps testable With this commit we're introducing the `stepFactory` and interfaces and implementations for each different kind of step (run, docker, local and remote actions). Separating each step kind into its own interface and implementation makes it easier to reason about and to change behaviour of the step. By introducing interfaces we enable better unit testability as now each step implementation, the step factory and the job executor can be tested on their own by mocking out parts that are irrelevant. This commits prepares us for implementing pre/post actions in a later PR. Co-authored-by: Marcus Noll <marcus.noll@new-work.se> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Robert Kowalski <robert.kowalski@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Jonas Holland <jonas.holland@new-work.se> * fix: run post steps in reverse order * test: add missing asserts for mocks * refactor: use local reference instead of function This may make code more easy to follow. * refactor: correct typo in function name * test: use named structs * test: only expected valid calls There are mocks which are only called on certain conditions. * refactor: use step-model to get step name Using the step-logger we have to get the logger name from the step model. * test: only mock stopContainer if required Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se> Co-authored-by: Marcus Noll <marcus.noll@new-work.se> Co-authored-by: Robert Kowalski <robert.kowalski@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Jonas Holland <jonas.holland@new-work.se> Co-authored-by: Casey Lee <cplee@nektos.com> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
parent
5d7027dc3f
commit
2bb3e74616
|
@ -1,27 +1,42 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ActionReader interface {
|
||||
readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader) (*model.Action, error)
|
||||
type actionStep interface {
|
||||
step
|
||||
|
||||
getActionModel() *model.Action
|
||||
}
|
||||
|
||||
type actionyamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||
type readAction func(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
|
||||
|
||||
type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||
type fileWriter func(filename string, data []byte, perm fs.FileMode) error
|
||||
|
||||
type runAction func(step actionStep, actionDir string, actionPath string, actionRepository string, actionRef string, localAction bool) common.Executor
|
||||
|
||||
//go:embed res/trampoline.js
|
||||
var trampoline embed.FS
|
||||
|
||||
func (sc *StepContext) readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||
func readActionImpl(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||
reader, closer, err := readFile("action.yml")
|
||||
if os.IsNotExist(err) {
|
||||
reader, closer, err = readFile("action.yaml")
|
||||
|
@ -82,3 +97,366 @@ func (sc *StepContext) readAction(step *model.Step, actionDir string, actionPath
|
|||
log.Debugf("Read action %v from '%s'", action, "Unknown")
|
||||
return action, err
|
||||
}
|
||||
|
||||
func runActionImpl(step actionStep, actionDir string, actionPath string, actionRepository string, actionRef string, localAction bool) common.Executor {
|
||||
rc := step.getRunContext()
|
||||
stepModel := step.getStepModel()
|
||||
return func(ctx context.Context) error {
|
||||
// Backup the parent composite action path and restore it on continue
|
||||
parentActionPath := rc.ActionPath
|
||||
parentActionRepository := rc.ActionRepository
|
||||
parentActionRef := rc.ActionRef
|
||||
defer func() {
|
||||
rc.ActionPath = parentActionPath
|
||||
rc.ActionRef = parentActionRef
|
||||
rc.ActionRepository = parentActionRepository
|
||||
}()
|
||||
rc.ActionRef = actionRef
|
||||
rc.ActionRepository = actionRepository
|
||||
action := step.getActionModel()
|
||||
log.Debugf("About to run action %v", action)
|
||||
|
||||
populateEnvsFromInput(step.getEnv(), action, rc)
|
||||
|
||||
actionLocation := ""
|
||||
if actionPath != "" {
|
||||
actionLocation = path.Join(actionDir, actionPath)
|
||||
} else {
|
||||
actionLocation = actionDir
|
||||
}
|
||||
actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
|
||||
|
||||
log.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", stepModel.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir)
|
||||
|
||||
maybeCopyToActionDir := func() error {
|
||||
rc.ActionPath = containerActionDir
|
||||
if stepModel.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, model.ActionRunsUsingNode16:
|
||||
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, *step.getEnv(), "", "")(ctx)
|
||||
case model.ActionRunsUsingDocker:
|
||||
return execAsDocker(ctx, action, actionName, containerActionDir, actionLocation, rc, step, localAction)
|
||||
case model.ActionRunsUsingComposite:
|
||||
return 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.ActionRunsUsingNode16,
|
||||
model.ActionRunsUsingComposite,
|
||||
}, action.Runs.Using))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO: break out parts of function to reduce complexicity
|
||||
// nolint:gocyclo
|
||||
func execAsDocker(ctx context.Context, action *model.Action, actionName string, containerLocation string, actionLocation string, rc *RunContext, step 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 = step.getRunContext().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)
|
||||
}
|
||||
}
|
||||
eval := step.getRunContext().NewStepExpressionEvaluator(step)
|
||||
cmd, err := shellquote.Split(eval.Interpolate(step.getStepModel().With["args"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cmd) == 0 {
|
||||
cmd = action.Runs.Args
|
||||
evalDockerArgs(step, action, &cmd)
|
||||
}
|
||||
entrypoint := strings.Fields(eval.Interpolate(step.getStepModel().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 := newStepContainer(ctx, step, 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 evalDockerArgs(step step, action *model.Action, cmd *[]string) {
|
||||
rc := step.getRunContext()
|
||||
stepModel := step.getStepModel()
|
||||
oldInputs := rc.Inputs
|
||||
defer func() {
|
||||
rc.Inputs = oldInputs
|
||||
}()
|
||||
inputs := make(map[string]interface{})
|
||||
eval := rc.NewExpressionEvaluator()
|
||||
// Set Defaults
|
||||
for k, input := range action.Inputs {
|
||||
inputs[k] = eval.Interpolate(input.Default)
|
||||
}
|
||||
if stepModel.With != nil {
|
||||
for k, v := range stepModel.With {
|
||||
inputs[k] = eval.Interpolate(v)
|
||||
}
|
||||
}
|
||||
rc.Inputs = inputs
|
||||
stepEE := rc.NewStepExpressionEvaluator(step)
|
||||
for i, v := range *cmd {
|
||||
(*cmd)[i] = stepEE.Interpolate(v)
|
||||
}
|
||||
mergeIntoMap(step.getEnv(), action.Runs.Env)
|
||||
|
||||
ee := rc.NewStepExpressionEvaluator(step)
|
||||
for k, v := range *step.getEnv() {
|
||||
(*step.getEnv())[k] = ee.Interpolate(v)
|
||||
}
|
||||
}
|
||||
|
||||
func newStepContainer(ctx context.Context, step step, image string, cmd []string, entrypoint []string) container.Container {
|
||||
rc := step.getRunContext()
|
||||
stepModel := step.getStepModel()
|
||||
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 *step.getEnv() {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", k, 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(), stepModel.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 execAsComposite(ctx context.Context, step step, _ string, rc *RunContext, containerActionDir string, actionName string, _ string, action *model.Action, maybeCopyToActionDir func() error) error {
|
||||
err := maybeCopyToActionDir()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Disable some features of composite actions, only for feature parity with github
|
||||
for _, compositeStep := range action.Runs.Steps {
|
||||
if err := compositeStep.Validate(rc.Config.CompositeRestrictions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
inputs := make(map[string]interface{})
|
||||
eval := step.getRunContext().NewExpressionEvaluator()
|
||||
// Set Defaults
|
||||
for k, input := range action.Inputs {
|
||||
inputs[k] = eval.Interpolate(input.Default)
|
||||
}
|
||||
if step.getStepModel().With != nil {
|
||||
for k, v := range step.getStepModel().With {
|
||||
inputs[k] = eval.Interpolate(v)
|
||||
}
|
||||
}
|
||||
// Doesn't work with the command processor has a pointer to the original rc
|
||||
// compositerc := rc.Clone()
|
||||
// Workaround start
|
||||
backup := *rc
|
||||
defer func() { *rc = backup }()
|
||||
*rc = *rc.Clone()
|
||||
scriptName := backup.CurrentStep
|
||||
for rcs := &backup; rcs.Parent != nil; rcs = rcs.Parent {
|
||||
scriptName = fmt.Sprintf("%s-composite-%s", rcs.Parent.CurrentStep, scriptName)
|
||||
}
|
||||
compositerc := rc
|
||||
compositerc.Parent = &RunContext{
|
||||
CurrentStep: scriptName,
|
||||
}
|
||||
// Workaround end
|
||||
compositerc.Composite = action
|
||||
envToEvaluate := mergeMaps(compositerc.Env, step.getStepModel().Environment())
|
||||
compositerc.Env = make(map[string]string)
|
||||
// origEnvMap: is used to pass env changes back to parent runcontext
|
||||
origEnvMap := make(map[string]string)
|
||||
for k, v := range envToEvaluate {
|
||||
ev := eval.Interpolate(v)
|
||||
origEnvMap[k] = ev
|
||||
compositerc.Env[k] = ev
|
||||
}
|
||||
compositerc.Inputs = inputs
|
||||
compositerc.ExprEval = compositerc.NewExpressionEvaluator()
|
||||
err = compositerc.CompositeExecutor()(ctx)
|
||||
|
||||
// Map outputs to parent rc
|
||||
eval = compositerc.NewStepExpressionEvaluator(step)
|
||||
for outputName, output := range action.Outputs {
|
||||
backup.setOutput(ctx, map[string]string{
|
||||
"name": outputName,
|
||||
}, eval.Interpolate(output.Value))
|
||||
}
|
||||
|
||||
backup.Masks = append(backup.Masks, compositerc.Masks...)
|
||||
// Test if evaluated parent env was altered by this composite step
|
||||
// Known Issues:
|
||||
// - you try to set an env variable to the same value as a scoped step env, will be discared
|
||||
for k, v := range compositerc.Env {
|
||||
if ov, ok := origEnvMap[k]; !ok || ov != v {
|
||||
backup.Env[k] = v
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func populateEnvsFromInput(env *map[string]string, 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 := (*env)[envKey]; !ok {
|
||||
(*env)[envKey] = rc.ExprEval.Interpolate(input.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
@ -121,13 +122,92 @@ runs:
|
|||
return nil
|
||||
}
|
||||
|
||||
closerMock.On("Close")
|
||||
if tt.filename != "" {
|
||||
closerMock.On("Close")
|
||||
}
|
||||
|
||||
sc := &StepContext{}
|
||||
action, err := sc.readAction(tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||
action, err := readActionImpl(tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.expected, action)
|
||||
|
||||
closerMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type exprEvalMock struct {
|
||||
ExpressionEvaluator
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (e *exprEvalMock) Interpolate(expr string) string {
|
||||
args := e.Called(expr)
|
||||
return args.String(0)
|
||||
}
|
||||
|
||||
func TestActionRunner(t *testing.T) {
|
||||
table := []struct {
|
||||
name string
|
||||
step actionStep
|
||||
}{
|
||||
{
|
||||
name: "Test",
|
||||
step: &stepActionRemote{
|
||||
Step: &model.Step{
|
||||
Uses: "repo@ref",
|
||||
},
|
||||
RunContext: &RunContext{
|
||||
ActionRepository: "repo",
|
||||
ActionPath: "path",
|
||||
ActionRef: "ref",
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "job",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"job": {
|
||||
Name: "job",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
action: &model.Action{
|
||||
Inputs: map[string]model.Input{
|
||||
"key": {
|
||||
Default: "default value",
|
||||
},
|
||||
},
|
||||
Runs: model.ActionRuns{
|
||||
Using: "node16",
|
||||
},
|
||||
},
|
||||
env: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cm := &containerMock{}
|
||||
cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil })
|
||||
cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, map[string]string{"INPUT_KEY": "default value"}, "", "").Return(func(ctx context.Context) error { return nil })
|
||||
tt.step.getRunContext().JobContainer = cm
|
||||
|
||||
ee := &exprEvalMock{}
|
||||
ee.On("Interpolate", "default value").Return("default value")
|
||||
tt.step.getRunContext().ExprEval = ee
|
||||
|
||||
_, localAction := tt.step.(*stepActionRemote)
|
||||
|
||||
err := runActionImpl(tt.step, "dir", "path", "repo", "ref", localAction)(ctx)
|
||||
|
||||
assert.Nil(t, err)
|
||||
ee.AssertExpectations(t)
|
||||
cm.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type containerMock struct {
|
||||
mock.Mock
|
||||
container.Container
|
||||
}
|
||||
|
||||
func (cm *containerMock) Create(capAdd []string, capDrop []string) common.Executor {
|
||||
args := cm.Called(capAdd, capDrop)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Pull(forcePull bool) common.Executor {
|
||||
args := cm.Called(forcePull)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Start(attach bool) common.Executor {
|
||||
args := cm.Called(attach)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Remove() common.Executor {
|
||||
args := cm.Called()
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Close() common.Executor {
|
||||
args := cm.Called()
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
|
||||
args := cm.Called(srcPath, env)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) UpdateFromImageEnv(env *map[string]string) common.Executor {
|
||||
args := cm.Called(env)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) UpdateFromPath(env *map[string]string) common.Executor {
|
||||
args := cm.Called(env)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor {
|
||||
args := cm.Called(destPath, files)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (cm *containerMock) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
|
||||
args := cm.Called(destPath, srcPath, useGitIgnore)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
func (cm *containerMock) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||
args := cm.Called(command, env, user, workdir)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
|
@ -70,8 +70,7 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
|||
}
|
||||
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||
rc := sc.RunContext
|
||||
func (rc *RunContext) NewStepExpressionEvaluator(step step) ExpressionEvaluator {
|
||||
// todo: cleanup EvaluationEnvironment creation
|
||||
job := rc.Run.Job()
|
||||
strategy := make(map[string]interface{})
|
||||
|
@ -97,7 +96,7 @@ func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
|||
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: rc.getGithubContext(),
|
||||
Env: rc.GetEnv(),
|
||||
Env: *step.getEnv(),
|
||||
Job: rc.getJobContext(),
|
||||
Steps: rc.getStepsContext(),
|
||||
Runner: map[string]interface{}{
|
||||
|
|
|
@ -147,13 +147,13 @@ func TestEvaluateRunContext(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEvaluateStepContext(t *testing.T) {
|
||||
func TestEvaluateStep(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
|
||||
sc := &StepContext{
|
||||
step := &stepRun{
|
||||
RunContext: rc,
|
||||
}
|
||||
ee := sc.NewExpressionEvaluator()
|
||||
|
||||
ee := rc.NewStepExpressionEvaluator(step)
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
|
|
|
@ -14,13 +14,14 @@ type jobInfo interface {
|
|||
startContainer() common.Executor
|
||||
stopContainer() common.Executor
|
||||
closeContainer() common.Executor
|
||||
newStepExecutor(step *model.Step) common.Executor
|
||||
interpolateOutputs() common.Executor
|
||||
result(result string)
|
||||
}
|
||||
|
||||
func newJobExecutor(info jobInfo) common.Executor {
|
||||
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
||||
steps := make([]common.Executor, 0)
|
||||
preSteps := make([]common.Executor, 0)
|
||||
postSteps := make([]common.Executor, 0)
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
if len(info.matrix()) > 0 {
|
||||
|
@ -29,15 +30,30 @@ func newJobExecutor(info jobInfo) common.Executor {
|
|||
return nil
|
||||
})
|
||||
|
||||
steps = append(steps, info.startContainer())
|
||||
infoSteps := info.steps()
|
||||
|
||||
for i, step := range info.steps() {
|
||||
if step.ID == "" {
|
||||
step.ID = fmt.Sprintf("%d", i)
|
||||
if len(infoSteps) == 0 {
|
||||
return common.NewDebugExecutor("No steps found")
|
||||
}
|
||||
|
||||
preSteps = append(preSteps, info.startContainer())
|
||||
|
||||
for i, stepModel := range infoSteps {
|
||||
if stepModel.ID == "" {
|
||||
stepModel.ID = fmt.Sprintf("%d", i)
|
||||
}
|
||||
stepExec := info.newStepExecutor(step)
|
||||
|
||||
step, err := sf.newStep(stepModel, rc)
|
||||
|
||||
if err != nil {
|
||||
return common.NewErrorExecutor(err)
|
||||
}
|
||||
|
||||
preSteps = append(preSteps, step.pre())
|
||||
|
||||
stepExec := step.main()
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
stepName := step.String()
|
||||
stepName := stepModel.String()
|
||||
return (func(ctx context.Context) error {
|
||||
err := stepExec(ctx)
|
||||
if err != nil {
|
||||
|
@ -50,9 +66,11 @@ func newJobExecutor(info jobInfo) common.Executor {
|
|||
return nil
|
||||
})(withStepLogger(ctx, stepName))
|
||||
})
|
||||
|
||||
postSteps = append([]common.Executor{step.post()}, postSteps...)
|
||||
}
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
postSteps = append(postSteps, func(ctx context.Context) error {
|
||||
jobError := common.JobError(ctx)
|
||||
if jobError != nil {
|
||||
info.result("failure")
|
||||
|
@ -67,5 +85,10 @@ func newJobExecutor(info jobInfo) common.Executor {
|
|||
return nil
|
||||
})
|
||||
|
||||
return common.NewPipelineExecutor(steps...).Finally(info.interpolateOutputs()).Finally(info.closeContainer())
|
||||
pipeline := make([]common.Executor, 0)
|
||||
pipeline = append(pipeline, preSteps...)
|
||||
pipeline = append(pipeline, steps...)
|
||||
pipeline = append(pipeline, postSteps...)
|
||||
|
||||
return common.NewPipelineExecutor(pipeline...).Finally(info.interpolateOutputs()).Finally(info.closeContainer())
|
||||
}
|
||||
|
|
|
@ -11,80 +11,105 @@ import (
|
|||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestJobExecutor(t *testing.T) {
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": baseImage,
|
||||
}
|
||||
tables := []TestJobFileInfo{
|
||||
{"testdata", "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, ""},
|
||||
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, ""},
|
||||
{"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, ""},
|
||||
{"testdata", "uses-github-root", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-path", "push", "", platforms, ""},
|
||||
{"testdata", "uses-docker-url", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-full-sha", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, ""},
|
||||
}
|
||||
// These tests are sufficient to only check syntax.
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
for _, table := range tables {
|
||||
runTestJobFile(ctx, t, table)
|
||||
}
|
||||
}
|
||||
|
||||
type jobInfoMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) matrix() map[string]interface{} {
|
||||
args := jpm.Called()
|
||||
func (jim *jobInfoMock) matrix() map[string]interface{} {
|
||||
args := jim.Called()
|
||||
return args.Get(0).(map[string]interface{})
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) steps() []*model.Step {
|
||||
args := jpm.Called()
|
||||
func (jim *jobInfoMock) steps() []*model.Step {
|
||||
args := jim.Called()
|
||||
|
||||
return args.Get(0).([]*model.Step)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) startContainer() common.Executor {
|
||||
args := jpm.Called()
|
||||
func (jim *jobInfoMock) startContainer() common.Executor {
|
||||
args := jim.Called()
|
||||
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) stopContainer() common.Executor {
|
||||
args := jpm.Called()
|
||||
func (jim *jobInfoMock) stopContainer() common.Executor {
|
||||
args := jim.Called()
|
||||
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) closeContainer() common.Executor {
|
||||
args := jpm.Called()
|
||||
func (jim *jobInfoMock) closeContainer() common.Executor {
|
||||
args := jim.Called()
|
||||
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) newStepExecutor(step *model.Step) common.Executor {
|
||||
args := jpm.Called(step)
|
||||
func (jim *jobInfoMock) interpolateOutputs() common.Executor {
|
||||
args := jim.Called()
|
||||
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) interpolateOutputs() common.Executor {
|
||||
args := jpm.Called()
|
||||
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
func (jim *jobInfoMock) result(result string) {
|
||||
jim.Called(result)
|
||||
}
|
||||
|
||||
func (jpm *jobInfoMock) result(result string) {
|
||||
jpm.Called(result)
|
||||
type stepFactoryMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (sfm *stepFactoryMock) newStep(model *model.Step, rc *RunContext) (step, error) {
|
||||
args := sfm.Called(model, rc)
|
||||
return args.Get(0).(step), args.Error(1)
|
||||
}
|
||||
|
||||
func TestNewJobExecutor(t *testing.T) {
|
||||
table := []struct {
|
||||
name string
|
||||
steps []*model.Step
|
||||
preSteps []bool
|
||||
postSteps []bool
|
||||
executedSteps []string
|
||||
result string
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "zeroSteps",
|
||||
steps: []*model.Step{},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"stopContainer",
|
||||
"interpolateOutputs",
|
||||
"closeContainer",
|
||||
},
|
||||
result: "success",
|
||||
hasError: false,
|
||||
name: "zeroSteps",
|
||||
steps: []*model.Step{},
|
||||
preSteps: []bool{},
|
||||
postSteps: []bool{},
|
||||
executedSteps: []string{},
|
||||
result: "success",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "stepWithoutPrePost",
|
||||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}},
|
||||
preSteps: []bool{false},
|
||||
postSteps: []bool{false},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"step1",
|
||||
|
@ -100,6 +125,8 @@ func TestNewJobExecutor(t *testing.T) {
|
|||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}},
|
||||
preSteps: []bool{false},
|
||||
postSteps: []bool{false},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"step1",
|
||||
|
@ -110,16 +137,80 @@ func TestNewJobExecutor(t *testing.T) {
|
|||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "multipleSteps",
|
||||
name: "stepWithPre",
|
||||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}},
|
||||
preSteps: []bool{true},
|
||||
postSteps: []bool{false},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"pre1",
|
||||
"step1",
|
||||
"stopContainer",
|
||||
"interpolateOutputs",
|
||||
"closeContainer",
|
||||
},
|
||||
result: "success",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "stepWithPost",
|
||||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}},
|
||||
preSteps: []bool{false},
|
||||
postSteps: []bool{true},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"step1",
|
||||
"post1",
|
||||
"stopContainer",
|
||||
"interpolateOutputs",
|
||||
"closeContainer",
|
||||
},
|
||||
result: "success",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "stepWithPreAndPost",
|
||||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}},
|
||||
preSteps: []bool{true},
|
||||
postSteps: []bool{true},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"pre1",
|
||||
"step1",
|
||||
"post1",
|
||||
"stopContainer",
|
||||
"interpolateOutputs",
|
||||
"closeContainer",
|
||||
},
|
||||
result: "success",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "stepsWithPreAndPost",
|
||||
steps: []*model.Step{{
|
||||
ID: "1",
|
||||
}, {
|
||||
ID: "2",
|
||||
}, {
|
||||
ID: "3",
|
||||
}},
|
||||
preSteps: []bool{true, false, true},
|
||||
postSteps: []bool{false, true, true},
|
||||
executedSteps: []string{
|
||||
"startContainer",
|
||||
"pre1",
|
||||
"pre3",
|
||||
"step1",
|
||||
"step2",
|
||||
"step3",
|
||||
"post3",
|
||||
"post2",
|
||||
"stopContainer",
|
||||
"interpolateOutputs",
|
||||
"closeContainer",
|
||||
|
@ -129,54 +220,95 @@ func TestNewJobExecutor(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
contains := func(needle string, haystack []string) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
jpm := &jobInfoMock{}
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
rc := &RunContext{}
|
||||
executorOrder := make([]string, 0)
|
||||
|
||||
jpm.On("startContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "startContainer")
|
||||
return nil
|
||||
})
|
||||
jim.On("steps").Return(tt.steps)
|
||||
|
||||
jpm.On("steps").Return(tt.steps)
|
||||
|
||||
for _, stepMock := range tt.steps {
|
||||
func(stepMock *model.Step) {
|
||||
jpm.On("newStepExecutor", stepMock).Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "step"+stepMock.ID)
|
||||
if tt.hasError {
|
||||
return fmt.Errorf("error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}(stepMock)
|
||||
if len(tt.steps) > 0 {
|
||||
jim.On("startContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "startContainer")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
jpm.On("interpolateOutputs").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "interpolateOutputs")
|
||||
return nil
|
||||
})
|
||||
for i, stepModel := range tt.steps {
|
||||
i := i
|
||||
stepModel := stepModel
|
||||
|
||||
jpm.On("matrix").Return(map[string]interface{}{})
|
||||
sm := &stepMock{}
|
||||
|
||||
jpm.On("stopContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "stopContainer")
|
||||
return nil
|
||||
})
|
||||
sfm.On("newStep", stepModel, rc).Return(sm, nil)
|
||||
|
||||
jpm.On("result", tt.result)
|
||||
sm.On("pre").Return(func(ctx context.Context) error {
|
||||
if tt.preSteps[i] {
|
||||
executorOrder = append(executorOrder, "pre"+stepModel.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
jpm.On("closeContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "closeContainer")
|
||||
return nil
|
||||
})
|
||||
sm.On("main").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "step"+stepModel.ID)
|
||||
if tt.hasError {
|
||||
return fmt.Errorf("error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
executor := newJobExecutor(jpm)
|
||||
sm.On("post").Return(func(ctx context.Context) error {
|
||||
if tt.postSteps[i] {
|
||||
executorOrder = append(executorOrder, "post"+stepModel.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
defer sm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
if len(tt.steps) > 0 {
|
||||
jim.On("matrix").Return(map[string]interface{}{})
|
||||
|
||||
jim.On("interpolateOutputs").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "interpolateOutputs")
|
||||
return nil
|
||||
})
|
||||
|
||||
if contains("stopContainer", tt.executedSteps) {
|
||||
jim.On("stopContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "stopContainer")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
jim.On("result", tt.result)
|
||||
|
||||
jim.On("closeContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "closeContainer")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
executor := newJobExecutor(jim, sfm, rc)
|
||||
err := executor(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.executedSteps, executorOrder)
|
||||
|
||||
jim.AssertExpectations(t)
|
||||
sfm.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -287,7 +287,7 @@ func (rc *RunContext) Executor() common.Executor {
|
|||
}
|
||||
|
||||
if isEnabled {
|
||||
return newJobExecutor(rc)(ctx)
|
||||
return newJobExecutor(rc, &stepFactoryImpl{}, rc)(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -298,12 +298,23 @@ func (rc *RunContext) Executor() common.Executor {
|
|||
func (rc *RunContext) CompositeExecutor() common.Executor {
|
||||
steps := make([]common.Executor, 0)
|
||||
|
||||
sf := &stepFactoryImpl{}
|
||||
|
||||
for i, step := range rc.Composite.Runs.Steps {
|
||||
if step.ID == "" {
|
||||
step.ID = fmt.Sprintf("%d", i)
|
||||
}
|
||||
|
||||
// create a copy of the step, since this composite action could
|
||||
// run multiple times and we might modify the instance
|
||||
stepcopy := step
|
||||
stepExec := rc.newStepExecutor(&stepcopy)
|
||||
|
||||
step, err := sf.newStep(&stepcopy, rc)
|
||||
if err != nil {
|
||||
return common.NewErrorExecutor(err)
|
||||
}
|
||||
stepExec := common.NewPipelineExecutor(step.pre(), step.main(), step.post())
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
err := stepExec(ctx)
|
||||
if err != nil {
|
||||
|
@ -323,59 +334,6 @@ func (rc *RunContext) CompositeExecutor() common.Executor {
|
|||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
||||
sc := &StepContext{
|
||||
RunContext: rc,
|
||||
Step: step,
|
||||
}
|
||||
return func(ctx context.Context) error {
|
||||
rc.CurrentStep = sc.Step.ID
|
||||
rc.StepResults[rc.CurrentStep] = &model.StepResult{
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outputs: make(map[string]string),
|
||||
}
|
||||
|
||||
runStep, err := sc.isEnabled(ctx)
|
||||
if err != nil {
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
return err
|
||||
}
|
||||
|
||||
if !runStep {
|
||||
log.Debugf("Skipping step '%s' due to '%s'", sc.Step.String(), sc.Step.If.Value)
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
||||
return nil
|
||||
}
|
||||
|
||||
exprEval, err := sc.setupEnv(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.ExprEval = exprEval
|
||||
|
||||
common.Logger(ctx).Infof("\u2B50 Run %s", sc.Step)
|
||||
err = sc.Executor(ctx)(ctx)
|
||||
if err == nil {
|
||||
common.Logger(ctx).Infof(" \u2705 Success - %s", sc.Step)
|
||||
} else {
|
||||
common.Logger(ctx).Errorf(" \u274C Failure - %s", sc.Step)
|
||||
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
if sc.Step.ContinueOnError {
|
||||
common.Logger(ctx).Infof("Failed but continue next step")
|
||||
err = nil
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess
|
||||
} else {
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) platformImage() string {
|
||||
job := rc.Run.Job()
|
||||
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type step interface {
|
||||
pre() common.Executor
|
||||
main() common.Executor
|
||||
post() common.Executor
|
||||
|
||||
getRunContext() *RunContext
|
||||
getStepModel() *model.Step
|
||||
getEnv() *map[string]string
|
||||
}
|
||||
|
||||
func runStepExecutor(step step, executor common.Executor) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
rc := step.getRunContext()
|
||||
stepModel := step.getStepModel()
|
||||
|
||||
rc.CurrentStep = stepModel.ID
|
||||
rc.StepResults[rc.CurrentStep] = &model.StepResult{
|
||||
Outcome: model.StepStatusSuccess,
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
Outputs: make(map[string]string),
|
||||
}
|
||||
|
||||
err := setupEnv(ctx, step)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runStep, err := isStepEnabled(ctx, step)
|
||||
if err != nil {
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
return err
|
||||
}
|
||||
|
||||
if !runStep {
|
||||
log.Debugf("Skipping step '%s' due to '%s'", stepModel.String(), stepModel.If.Value)
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
||||
return nil
|
||||
}
|
||||
|
||||
common.Logger(ctx).Infof("\u2B50 Run %s", stepModel)
|
||||
|
||||
err = executor(ctx)
|
||||
|
||||
if err == nil {
|
||||
common.Logger(ctx).Infof(" \u2705 Success - %s", stepModel)
|
||||
} else {
|
||||
common.Logger(ctx).Errorf(" \u274C Failure - %s", stepModel)
|
||||
|
||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||
if stepModel.ContinueOnError {
|
||||
common.Logger(ctx).Infof("Failed but continue next step")
|
||||
err = nil
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSuccess
|
||||
} else {
|
||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func setupEnv(ctx context.Context, step step) error {
|
||||
rc := step.getRunContext()
|
||||
|
||||
mergeEnv(step)
|
||||
err := rc.JobContainer.UpdateFromImageEnv(step.getEnv())(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rc.JobContainer.UpdateFromEnv((*step.getEnv())["GITHUB_ENV"], step.getEnv())(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rc.JobContainer.UpdateFromPath(step.getEnv())(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergeIntoMap(step.getEnv(), step.getStepModel().GetEnv()) // step env should not be overwritten
|
||||
|
||||
exprEval := rc.NewStepExpressionEvaluator(step)
|
||||
for k, v := range *step.getEnv() {
|
||||
(*step.getEnv())[k] = exprEval.Interpolate(v)
|
||||
}
|
||||
|
||||
common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeEnv(step step) {
|
||||
env := step.getEnv()
|
||||
rc := step.getRunContext()
|
||||
job := rc.Run.Job()
|
||||
|
||||
c := job.Container()
|
||||
if c != nil {
|
||||
mergeIntoMap(env, rc.GetEnv(), c.Env)
|
||||
} else {
|
||||
mergeIntoMap(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
|
||||
}
|
||||
|
||||
mergeIntoMap(env, rc.withGithubEnv(*env))
|
||||
}
|
||||
|
||||
func isStepEnabled(ctx context.Context, step step) (bool, error) {
|
||||
rc := step.getRunContext()
|
||||
|
||||
runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), step.getStepModel().If.Value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", step.getStepModel().If.Value, err)
|
||||
}
|
||||
|
||||
return runStep, nil
|
||||
}
|
||||
|
||||
func mergeIntoMap(target *map[string]string, maps ...map[string]string) {
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
(*target)[k] = v
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type stepActionLocal struct {
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
runAction runAction
|
||||
readAction readAction
|
||||
env map[string]string
|
||||
action *model.Action
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) pre() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) main() common.Executor {
|
||||
sal.env = map[string]string{}
|
||||
|
||||
return runStepExecutor(sal, func(ctx context.Context) error {
|
||||
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||
|
||||
localReader := func(ctx context.Context) actionYamlReader {
|
||||
_, cpath := getContainerActionPaths(sal.Step, path.Join(actionDir, ""), sal.RunContext)
|
||||
return func(filename string) (io.Reader, io.Closer, error) {
|
||||
tars, err := sal.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
|
||||
}
|
||||
}
|
||||
|
||||
actionModel, err := sal.readAction(sal.Step, actionDir, "", localReader(ctx), ioutil.WriteFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sal.action = actionModel
|
||||
log.Debugf("Read action %v from '%s'", sal.action, "Unknown")
|
||||
|
||||
return sal.runAction(sal, actionDir, "", "", "", true)(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) post() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getRunContext() *RunContext {
|
||||
return sal.RunContext
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getStepModel() *model.Step {
|
||||
return sal.Step
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getEnv() *map[string]string {
|
||||
return &sal.env
|
||||
}
|
||||
|
||||
func (sal *stepActionLocal) getActionModel() *model.Action {
|
||||
return sal.action
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type stepActionLocalMocks struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (salm *stepActionLocalMocks) runAction(step actionStep, actionDir string, actionPath string, actionRepository string, actionRef string, localAction bool) common.Executor {
|
||||
args := salm.Called(step, actionDir, actionPath, actionRepository, actionRef, localAction)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (salm *stepActionLocalMocks) readAction(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||
args := salm.Called(step, actionDir, actionPath, readFile, writeFile)
|
||||
return args.Get(0).(*model.Action), args.Error(1)
|
||||
}
|
||||
|
||||
func TestStepActionLocalTest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cm := &containerMock{}
|
||||
salm := &stepActionLocalMocks{}
|
||||
|
||||
sal := &stepActionLocal{
|
||||
readAction: salm.readAction,
|
||||
runAction: salm.runAction,
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
ExprEval: &expressionEvaluator{},
|
||||
Config: &Config{
|
||||
Workdir: "/tmp",
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {
|
||||
Defaults: model.Defaults{
|
||||
Run: model.RunDefaults{
|
||||
Shell: "bash",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Uses: "./path/to/action",
|
||||
},
|
||||
}
|
||||
|
||||
salm.On("readAction", sal.Step, "/tmp/path/to/action", "", mock.Anything, mock.Anything).
|
||||
Return(&model.Action{}, nil)
|
||||
|
||||
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
salm.On("runAction", sal, "/tmp/path/to/action", "", "", "", true).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
err := sal.main()(ctx)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
salm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepActionLocalPrePost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
sal := &stepActionLocal{}
|
||||
|
||||
err := sal.pre()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = sal.post()(ctx)
|
||||
assert.Nil(t, err)
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type stepActionRemote struct {
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
readAction readAction
|
||||
runAction runAction
|
||||
action *model.Action
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) pre() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
stepActionRemoteNewCloneExecutor = common.NewGitCloneExecutor
|
||||
)
|
||||
|
||||
func (sar *stepActionRemote) main() common.Executor {
|
||||
sar.env = map[string]string{}
|
||||
|
||||
return runStepExecutor(sar, func(ctx context.Context) error {
|
||||
remoteAction := newRemoteAction(sar.Step.Uses)
|
||||
if remoteAction == nil {
|
||||
return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses)
|
||||
}
|
||||
|
||||
remoteAction.URL = sar.RunContext.Config.GitHubInstance
|
||||
|
||||
github := sar.RunContext.getGithubContext()
|
||||
if remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
|
||||
return nil
|
||||
}
|
||||
|
||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
||||
gitClone := stepActionRemoteNewCloneExecutor(common.NewGitCloneExecutorInput{
|
||||
URL: remoteAction.CloneURL(),
|
||||
Ref: remoteAction.Ref,
|
||||
Dir: actionDir,
|
||||
Token: github.Token,
|
||||
})
|
||||
var ntErr common.Executor
|
||||
if err := gitClone(ctx); err != nil {
|
||||
if err.Error() == "short SHA references are not supported" {
|
||||
err = errors.Cause(err)
|
||||
return 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", sar.Step.Uses, remoteAction.Ref, err.Error())
|
||||
} else if err.Error() != "some refs were not updated" {
|
||||
return err
|
||||
} else {
|
||||
ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
remoteReader := func(ctx context.Context) actionYamlReader {
|
||||
return func(filename string) (io.Reader, io.Closer, error) {
|
||||
f, err := os.Open(filepath.Join(actionDir, remoteAction.Path, filename))
|
||||
return f, f, err
|
||||
}
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
ntErr,
|
||||
func(ctx context.Context) error {
|
||||
actionModel, err := sar.readAction(sar.Step, actionDir, remoteAction.Path, remoteReader(ctx), ioutil.WriteFile)
|
||||
sar.action = actionModel
|
||||
log.Debugf("Read action %v from '%s'", sar.action, "Unknown")
|
||||
return err
|
||||
},
|
||||
sar.runAction(sar, actionDir, remoteAction.Path, remoteAction.Repo, remoteAction.Ref, false),
|
||||
)(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) post() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getRunContext() *RunContext {
|
||||
return sar.RunContext
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getStepModel() *model.Step {
|
||||
return sar.Step
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getEnv() *map[string]string {
|
||||
return &sar.env
|
||||
}
|
||||
|
||||
func (sar *stepActionRemote) getActionModel() *model.Action {
|
||||
return sar.action
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type stepActionRemoteMocks struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (sarm *stepActionRemoteMocks) readAction(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||
args := sarm.Called(step, actionDir, actionPath, readFile, writeFile)
|
||||
return args.Get(0).(*model.Action), args.Error(1)
|
||||
}
|
||||
|
||||
func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string, actionPath string, actionRepository string, actionRef string, localAction bool) common.Executor {
|
||||
args := sarm.Called(step, actionDir, actionPath, actionRepository, actionRef, localAction)
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func TestStepActionRemoteTest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cm := &containerMock{}
|
||||
|
||||
sarm := &stepActionRemoteMocks{}
|
||||
|
||||
clonedAction := false
|
||||
|
||||
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
|
||||
stepActionRemoteNewCloneExecutor = func(input common.NewGitCloneExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
clonedAction = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer (func() {
|
||||
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
|
||||
})()
|
||||
|
||||
sar := &stepActionRemote{
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
GitHubInstance: "https://github.com",
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
JobContainer: cm,
|
||||
},
|
||||
Step: &model.Step{
|
||||
Uses: "remote/action@v1",
|
||||
},
|
||||
readAction: sarm.readAction,
|
||||
runAction: sarm.runAction,
|
||||
}
|
||||
|
||||
suffixMatcher := func(suffix string) interface{} {
|
||||
return mock.MatchedBy(func(actionDir string) bool {
|
||||
return strings.HasSuffix(actionDir, suffix)
|
||||
})
|
||||
}
|
||||
|
||||
cm.On("UpdateFromImageEnv", &sar.env).Return(func(ctx context.Context) error { return nil })
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &sar.env).Return(func(ctx context.Context) error { return nil })
|
||||
cm.On("UpdateFromPath", &sar.env).Return(func(ctx context.Context) error { return nil })
|
||||
|
||||
sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||
sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), "", "action", "v1", false).Return(func(ctx context.Context) error { return nil })
|
||||
|
||||
err := sar.main()(ctx)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, clonedAction)
|
||||
sarm.AssertExpectations(t)
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepActionRemotePrePost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
sar := &stepActionRemote{}
|
||||
|
||||
err := sar.pre()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = sar.post()(ctx)
|
||||
assert.Nil(t, err)
|
||||
}
|
|
@ -1,751 +0,0 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"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(ctx context.Context) common.Executor {
|
||||
rc := sc.RunContext
|
||||
step := sc.Step
|
||||
|
||||
switch step.Type() {
|
||||
case model.StepTypeRun:
|
||||
return common.NewPipelineExecutor(
|
||||
sc.setupShellCommandExecutor(),
|
||||
sc.execJobContainer(),
|
||||
)
|
||||
|
||||
case model.StepTypeUsesDockerURL:
|
||||
return common.NewPipelineExecutor(
|
||||
sc.runUsesContainer(),
|
||||
)
|
||||
|
||||
case model.StepTypeUsesActionLocal:
|
||||
actionDir := filepath.Join(rc.Config.Workdir, step.Uses)
|
||||
|
||||
localReader := func(ctx context.Context) actionyamlReader {
|
||||
_, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, ""), sc.RunContext)
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
sc.setupAction(actionDir, "", localReader),
|
||||
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() && isLocalCheckout(github, step) && !rc.Config.NoSkipCheckout {
|
||||
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(ctx); 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)
|
||||
}
|
||||
}
|
||||
|
||||
remoteReader := func(ctx context.Context) actionyamlReader {
|
||||
return func(filename string) (io.Reader, io.Closer, error) {
|
||||
f, err := os.Open(filepath.Join(actionDir, remoteAction.Path, filename))
|
||||
return f, f, err
|
||||
}
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
ntErr,
|
||||
sc.setupAction(actionDir, remoteAction.Path, remoteReader),
|
||||
sc.runAction(actionDir, remoteAction.Path, remoteAction.Repo, remoteAction.Ref, 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 {
|
||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", sc.Step.If.Value, 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
|
||||
}
|
||||
|
||||
func (sc *StepContext) setupWorkingDirectory() {
|
||||
rc := sc.RunContext
|
||||
step := sc.Step
|
||||
|
||||
if step.WorkingDirectory == "" {
|
||||
step.WorkingDirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
|
||||
}
|
||||
|
||||
// jobs can receive context values, so we interpolate
|
||||
step.WorkingDirectory = rc.ExprEval.Interpolate(step.WorkingDirectory)
|
||||
|
||||
// but top level keys in workflow file like `defaults` or `env` can't
|
||||
if step.WorkingDirectory == "" {
|
||||
step.WorkingDirectory = rc.Run.Workflow.Defaults.Run.WorkingDirectory
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) setupShell() {
|
||||
rc := sc.RunContext
|
||||
step := sc.Step
|
||||
|
||||
if step.Shell == "" {
|
||||
step.Shell = rc.Run.Job().Defaults.Run.Shell
|
||||
}
|
||||
|
||||
step.Shell = rc.ExprEval.Interpolate(step.Shell)
|
||||
|
||||
if step.Shell == "" {
|
||||
step.Shell = rc.Run.Workflow.Defaults.Run.Shell
|
||||
}
|
||||
|
||||
// current GitHub Runner behaviour is that default is `sh`,
|
||||
// but if it's not container it validates with `which` command
|
||||
// if `bash` is available, and provides `bash` if it is
|
||||
// for now I'm going to leave below logic, will address it in different PR
|
||||
// https://github.com/actions/runner/blob/9a829995e02d2db64efb939dc2f283002595d4d9/src/Runner.Worker/Handlers/ScriptHandler.cs#L87-L91
|
||||
if rc.Run.Job().Container() != nil {
|
||||
if rc.Run.Job().Container().Image != "" && step.Shell == "" {
|
||||
step.Shell = "sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 (sc *StepContext) setupShellCommand() (name, script string, err error) {
|
||||
sc.setupShell()
|
||||
sc.setupWorkingDirectory()
|
||||
|
||||
step := sc.Step
|
||||
|
||||
script = sc.RunContext.ExprEval.Interpolate(step.Run)
|
||||
|
||||
scCmd := step.ShellCommand()
|
||||
|
||||
name = getScriptName(sc.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 step.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"
|
||||
}
|
||||
|
||||
script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend)
|
||||
|
||||
log.Debugf("Wrote command \n%s\n to '%s'", script, name)
|
||||
|
||||
scriptPath := fmt.Sprintf("%s/%s", ActPath, name)
|
||||
sc.Cmd, err = shellquote.Split(strings.Replace(scCmd, `{0}`, scriptPath, 1))
|
||||
|
||||
return name, script, err
|
||||
}
|
||||
|
||||
func (sc *StepContext) setupShellCommandExecutor() common.Executor {
|
||||
rc := sc.RunContext
|
||||
return func(ctx context.Context) error {
|
||||
scriptName, script, err := sc.setupShellCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rc.JobContainer.Copy(ActPath, &container.FileEntry{
|
||||
Name: scriptName,
|
||||
Mode: 0755,
|
||||
Body: script,
|
||||
})(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))
|
||||
}
|
||||
|
||||
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://")
|
||||
eval := sc.RunContext.NewExpressionEvaluator()
|
||||
cmd, err := shellquote.Split(eval.Interpolate(step.With["args"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entrypoint := strings.Fields(eval.Interpolate(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)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) setupAction(actionDir string, actionPath string, reader func(context.Context) actionyamlReader) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
action, err := sc.readAction(sc.Step, actionDir, actionPath, reader(ctx), ioutil.WriteFile)
|
||||
sc.Action = action
|
||||
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
|
||||
}
|
||||
|
||||
func (sc *StepContext) runAction(actionDir string, actionPath string, actionRepository string, actionRef string, localAction bool) common.Executor {
|
||||
rc := sc.RunContext
|
||||
step := sc.Step
|
||||
return func(ctx context.Context) error {
|
||||
// Backup the parent composite action path and restore it on continue
|
||||
parentActionPath := rc.ActionPath
|
||||
parentActionRepository := rc.ActionRepository
|
||||
parentActionRef := rc.ActionRef
|
||||
defer func() {
|
||||
rc.ActionPath = parentActionPath
|
||||
rc.ActionRef = parentActionRef
|
||||
rc.ActionRepository = parentActionRepository
|
||||
}()
|
||||
rc.ActionRef = actionRef
|
||||
rc.ActionRepository = actionRepository
|
||||
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)
|
||||
|
||||
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 {
|
||||
rc.ActionPath = 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, model.ActionRunsUsingNode16:
|
||||
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.ActionRunsUsingNode16,
|
||||
model.ActionRunsUsingComposite,
|
||||
}, action.Runs.Using))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *StepContext) evalDockerArgs(action *model.Action, cmd *[]string) {
|
||||
rc := sc.RunContext
|
||||
step := sc.Step
|
||||
oldInputs := rc.Inputs
|
||||
defer func() {
|
||||
rc.Inputs = oldInputs
|
||||
}()
|
||||
inputs := make(map[string]interface{})
|
||||
eval := sc.RunContext.NewExpressionEvaluator()
|
||||
// Set Defaults
|
||||
for k, input := range action.Inputs {
|
||||
inputs[k] = eval.Interpolate(input.Default)
|
||||
}
|
||||
if step.With != nil {
|
||||
for k, v := range step.With {
|
||||
inputs[k] = eval.Interpolate(v)
|
||||
}
|
||||
}
|
||||
rc.Inputs = inputs
|
||||
stepEE := sc.NewExpressionEvaluator()
|
||||
for i, v := range *cmd {
|
||||
(*cmd)[i] = stepEE.Interpolate(v)
|
||||
}
|
||||
sc.Env = mergeMaps(sc.Env, action.Runs.Env)
|
||||
|
||||
ee := sc.NewExpressionEvaluator()
|
||||
for k, v := range sc.Env {
|
||||
sc.Env[k] = ee.Interpolate(v)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
eval := sc.NewExpressionEvaluator()
|
||||
cmd, err := shellquote.Split(eval.Interpolate(step.With["args"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cmd) == 0 {
|
||||
cmd = action.Runs.Args
|
||||
sc.evalDockerArgs(action, &cmd)
|
||||
}
|
||||
entrypoint := strings.Fields(eval.Interpolate(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
|
||||
}
|
||||
// Disable some features of composite actions, only for feature parity with github
|
||||
for _, compositeStep := range action.Runs.Steps {
|
||||
if err := compositeStep.Validate(rc.Config.CompositeRestrictions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
inputs := make(map[string]interface{})
|
||||
eval := sc.RunContext.NewExpressionEvaluator()
|
||||
// Set Defaults
|
||||
for k, input := range action.Inputs {
|
||||
inputs[k] = eval.Interpolate(input.Default)
|
||||
}
|
||||
if step.With != nil {
|
||||
for k, v := range step.With {
|
||||
inputs[k] = eval.Interpolate(v)
|
||||
}
|
||||
}
|
||||
// Doesn't work with the command processor has a pointer to the original rc
|
||||
// compositerc := rc.Clone()
|
||||
// Workaround start
|
||||
backup := *rc
|
||||
defer func() { *rc = backup }()
|
||||
*rc = *rc.Clone()
|
||||
scriptName := backup.CurrentStep
|
||||
for rcs := &backup; rcs.Parent != nil; rcs = rcs.Parent {
|
||||
scriptName = fmt.Sprintf("%s-composite-%s", rcs.Parent.CurrentStep, scriptName)
|
||||
}
|
||||
compositerc := rc
|
||||
compositerc.Parent = &RunContext{
|
||||
CurrentStep: scriptName,
|
||||
}
|
||||
// Workaround end
|
||||
compositerc.Composite = action
|
||||
envToEvaluate := mergeMaps(compositerc.Env, step.Environment())
|
||||
compositerc.Env = make(map[string]string)
|
||||
// origEnvMap: is used to pass env changes back to parent runcontext
|
||||
origEnvMap := make(map[string]string)
|
||||
for k, v := range envToEvaluate {
|
||||
ev := eval.Interpolate(v)
|
||||
origEnvMap[k] = ev
|
||||
compositerc.Env[k] = ev
|
||||
}
|
||||
compositerc.Inputs = inputs
|
||||
compositerc.ExprEval = compositerc.NewExpressionEvaluator()
|
||||
err = compositerc.CompositeExecutor()(ctx)
|
||||
|
||||
// Map outputs to parent rc
|
||||
eval = (&StepContext{
|
||||
Env: compositerc.Env,
|
||||
RunContext: compositerc,
|
||||
}).NewExpressionEvaluator()
|
||||
for outputName, output := range action.Outputs {
|
||||
backup.setOutput(ctx, map[string]string{
|
||||
"name": outputName,
|
||||
}, eval.Interpolate(output.Value))
|
||||
}
|
||||
|
||||
backup.Masks = append(backup.Masks, compositerc.Masks...)
|
||||
// Test if evaluated parent env was altered by this composite step
|
||||
// Known Issues:
|
||||
// - you try to set an env variable to the same value as a scoped step env, will be discared
|
||||
for k, v := range compositerc.Env {
|
||||
if ov, ok := origEnvMap[k]; !ok || ov != v {
|
||||
backup.Env[k] = v
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func TestStepContextExecutor(t *testing.T) {
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": baseImage,
|
||||
}
|
||||
tables := []TestJobFileInfo{
|
||||
{"testdata", "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms, ""},
|
||||
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, ""},
|
||||
{"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, ""},
|
||||
{"testdata", "uses-github-root", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-path", "push", "", platforms, ""},
|
||||
{"testdata", "uses-docker-url", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-full-sha", "push", "", platforms, ""},
|
||||
{"testdata", "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms, ""},
|
||||
}
|
||||
// These tests are sufficient to only check syntax.
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
for _, table := range tables {
|
||||
runTestJobFile(ctx, t, table)
|
||||
}
|
||||
}
|
||||
|
||||
func createIfTestStepContext(t *testing.T, input string) *StepContext {
|
||||
var step *model.Step
|
||||
err := yaml.Unmarshal([]byte(input), &step)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return &StepContext{
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
Platforms: map[string]string{
|
||||
"ubuntu-latest": "ubuntu-latest",
|
||||
},
|
||||
},
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
Env: map[string]string{},
|
||||
Run: &model.Run{
|
||||
JobID: "job1",
|
||||
Workflow: &model.Workflow{
|
||||
Name: "workflow1",
|
||||
Jobs: map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Step: step,
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepContextIsEnabled(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
// success()
|
||||
sc := createIfTestStepContext(t, "if: success()")
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: success()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: success()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.False(sc.isEnabled(context.Background()))
|
||||
|
||||
// failure()
|
||||
sc = createIfTestStepContext(t, "if: failure()")
|
||||
assertObject.False(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: failure()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.False(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: failure()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
|
||||
// always()
|
||||
sc = createIfTestStepContext(t, "if: always()")
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: always()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
|
||||
sc = createIfTestStepContext(t, "if: always()")
|
||||
sc.RunContext.StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(sc.isEnabled(context.Background()))
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
type stepDocker struct {
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (sd *stepDocker) pre() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sd *stepDocker) main() common.Executor {
|
||||
sd.env = map[string]string{}
|
||||
|
||||
return runStepExecutor(sd, sd.runUsesContainer())
|
||||
}
|
||||
|
||||
func (sd *stepDocker) post() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sd *stepDocker) getRunContext() *RunContext {
|
||||
return sd.RunContext
|
||||
}
|
||||
|
||||
func (sd *stepDocker) getStepModel() *model.Step {
|
||||
return sd.Step
|
||||
}
|
||||
|
||||
func (sd *stepDocker) getEnv() *map[string]string {
|
||||
return &sd.env
|
||||
}
|
||||
|
||||
func (sd *stepDocker) runUsesContainer() common.Executor {
|
||||
rc := sd.RunContext
|
||||
step := sd.Step
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
image := strings.TrimPrefix(step.Uses, "docker://")
|
||||
eval := rc.NewExpressionEvaluator()
|
||||
cmd, err := shellquote.Split(eval.Interpolate(step.With["args"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entrypoint := strings.Fields(eval.Interpolate(step.With["entrypoint"]))
|
||||
stepContainer := sd.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)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ContainerNewContainer = container.NewContainer
|
||||
)
|
||||
|
||||
func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd []string, entrypoint []string) container.Container {
|
||||
rc := sd.RunContext
|
||||
step := sd.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 sd.env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", k, 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 := ContainerNewContainer(&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
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestStepDockerMain(t *testing.T) {
|
||||
cm := &containerMock{}
|
||||
|
||||
var input *container.NewContainerInput
|
||||
|
||||
// mock the new container call
|
||||
origContainerNewContainer := ContainerNewContainer
|
||||
ContainerNewContainer = func(containerInput *container.NewContainerInput) container.Container {
|
||||
input = containerInput
|
||||
return cm
|
||||
}
|
||||
defer (func() {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
})()
|
||||
|
||||
sd := &stepDocker{
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {
|
||||
Defaults: model.Defaults{
|
||||
Run: model.RunDefaults{
|
||||
Shell: "bash",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Uses: "docker://node:14",
|
||||
WorkingDirectory: "workdir",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("Pull", false).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("Remove").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("Create", []string(nil), []string(nil)).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("Start", true).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("Close").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
err := sd.main()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "node:14", input.Image)
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepDockerPrePost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sd := &stepDocker{}
|
||||
|
||||
err := sd.pre()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = sd.post()(ctx)
|
||||
assert.Nil(t, err)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
type stepFactory interface {
|
||||
newStep(step *model.Step, rc *RunContext) (step, error)
|
||||
}
|
||||
|
||||
type stepFactoryImpl struct{}
|
||||
|
||||
func (sf *stepFactoryImpl) newStep(stepModel *model.Step, rc *RunContext) (step, error) {
|
||||
switch stepModel.Type() {
|
||||
case model.StepTypeInvalid:
|
||||
return nil, fmt.Errorf("Invalid run/uses syntax for job:%s step:%+v", rc.Run, stepModel)
|
||||
case model.StepTypeRun:
|
||||
return &stepRun{
|
||||
Step: stepModel,
|
||||
RunContext: rc,
|
||||
}, nil
|
||||
case model.StepTypeUsesActionLocal:
|
||||
return &stepActionLocal{
|
||||
Step: stepModel,
|
||||
RunContext: rc,
|
||||
readAction: readActionImpl,
|
||||
runAction: runActionImpl,
|
||||
}, nil
|
||||
case model.StepTypeUsesActionRemote:
|
||||
return &stepActionRemote{
|
||||
Step: stepModel,
|
||||
RunContext: rc,
|
||||
readAction: readActionImpl,
|
||||
runAction: runActionImpl,
|
||||
}, nil
|
||||
case model.StepTypeUsesDockerURL:
|
||||
return &stepDocker{
|
||||
Step: stepModel,
|
||||
RunContext: rc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, stepModel)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStepFactoryNewStep(t *testing.T) {
|
||||
table := []struct {
|
||||
name string
|
||||
model *model.Step
|
||||
check func(s step) bool
|
||||
}{
|
||||
{
|
||||
name: "StepRemoteAction",
|
||||
model: &model.Step{
|
||||
Uses: "remote/action@v1",
|
||||
},
|
||||
check: func(s step) bool {
|
||||
_, ok := s.(*stepActionRemote)
|
||||
return ok
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "StepLocalAction",
|
||||
model: &model.Step{
|
||||
Uses: "./action@v1",
|
||||
},
|
||||
check: func(s step) bool {
|
||||
_, ok := s.(*stepActionLocal)
|
||||
return ok
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "StepDocker",
|
||||
model: &model.Step{
|
||||
Uses: "docker://image:tag",
|
||||
},
|
||||
check: func(s step) bool {
|
||||
_, ok := s.(*stepDocker)
|
||||
return ok
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "StepRun",
|
||||
model: &model.Step{
|
||||
Run: "cmd",
|
||||
},
|
||||
check: func(s step) bool {
|
||||
_, ok := s.(*stepRun)
|
||||
return ok
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sf := &stepFactoryImpl{}
|
||||
|
||||
step, err := sf.newStep(tt.model, &RunContext{})
|
||||
|
||||
assert.True(t, tt.check((step)))
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepFactoryInvalidStep(t *testing.T) {
|
||||
model := &model.Step{
|
||||
Uses: "remote/action@v1",
|
||||
Run: "cmd",
|
||||
}
|
||||
|
||||
sf := &stepFactoryImpl{}
|
||||
|
||||
_, err := sf.newStep(model, &RunContext{})
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type stepRun struct {
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
cmd []string
|
||||
env map[string]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, common.NewPipelineExecutor(
|
||||
sr.setupShellCommandExecutor(),
|
||||
func(ctx context.Context) error {
|
||||
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.Step.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) getStepModel() *model.Step {
|
||||
return sr.Step
|
||||
}
|
||||
|
||||
func (sr *stepRun) getEnv() *map[string]string {
|
||||
return &sr.env
|
||||
}
|
||||
|
||||
func (sr *stepRun) setupShellCommandExecutor() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
scriptName, script, err := sr.setupShellCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sr.RunContext.JobContainer.Copy(ActPath, &container.FileEntry{
|
||||
Name: scriptName,
|
||||
Mode: 0755,
|
||||
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)
|
||||
}
|
||||
|
||||
// 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() (name, script string, err error) {
|
||||
sr.setupShell()
|
||||
sr.setupWorkingDirectory()
|
||||
|
||||
step := sr.Step
|
||||
|
||||
script = sr.RunContext.NewStepExpressionEvaluator(sr).Interpolate(step.Run)
|
||||
|
||||
scCmd := step.ShellCommand()
|
||||
|
||||
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 step.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"
|
||||
}
|
||||
|
||||
script = fmt.Sprintf("%s\n%s\n%s", runPrepend, script, runAppend)
|
||||
|
||||
log.Debugf("Wrote command \n%s\n to '%s'", script, name)
|
||||
|
||||
scriptPath := fmt.Sprintf("%s/%s", ActPath, name)
|
||||
sr.cmd, err = shellquote.Split(strings.Replace(scCmd, `{0}`, scriptPath, 1))
|
||||
|
||||
return name, script, err
|
||||
}
|
||||
|
||||
func (sr *stepRun) setupShell() {
|
||||
rc := sr.RunContext
|
||||
step := sr.Step
|
||||
|
||||
if step.Shell == "" {
|
||||
step.Shell = rc.Run.Job().Defaults.Run.Shell
|
||||
}
|
||||
|
||||
step.Shell = rc.ExprEval.Interpolate(step.Shell)
|
||||
|
||||
if step.Shell == "" {
|
||||
step.Shell = rc.Run.Workflow.Defaults.Run.Shell
|
||||
}
|
||||
|
||||
// current GitHub Runner behaviour is that default is `sh`,
|
||||
// but if it's not container it validates with `which` command
|
||||
// if `bash` is available, and provides `bash` if it is
|
||||
// for now I'm going to leave below logic, will address it in different PR
|
||||
// https://github.com/actions/runner/blob/9a829995e02d2db64efb939dc2f283002595d4d9/src/Runner.Worker/Handlers/ScriptHandler.cs#L87-L91
|
||||
if rc.Run.Job().Container() != nil {
|
||||
if rc.Run.Job().Container().Image != "" && step.Shell == "" {
|
||||
step.Shell = "sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *stepRun) setupWorkingDirectory() {
|
||||
rc := sr.RunContext
|
||||
step := sr.Step
|
||||
|
||||
if step.WorkingDirectory == "" {
|
||||
step.WorkingDirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
|
||||
}
|
||||
|
||||
// jobs can receive context values, so we interpolate
|
||||
step.WorkingDirectory = rc.ExprEval.Interpolate(step.WorkingDirectory)
|
||||
|
||||
// but top level keys in workflow file like `defaults` or `env` can't
|
||||
if step.WorkingDirectory == "" {
|
||||
step.WorkingDirectory = rc.Run.Workflow.Defaults.Run.WorkingDirectory
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestStepRun(t *testing.T) {
|
||||
cm := &containerMock{}
|
||||
fileEntry := &container.FileEntry{
|
||||
Name: "workflow/1.sh",
|
||||
Mode: 0755,
|
||||
Body: "\ncmd\n",
|
||||
}
|
||||
|
||||
sr := &stepRun{
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
ExprEval: &expressionEvaluator{},
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {
|
||||
Defaults: model.Defaults{
|
||||
Run: model.RunDefaults{
|
||||
Shell: "bash",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
},
|
||||
Step: &model.Step{
|
||||
ID: "1",
|
||||
Run: "cmd",
|
||||
WorkingDirectory: "workdir",
|
||||
},
|
||||
}
|
||||
|
||||
cm.On("Copy", "/var/run/act", []*container.FileEntry{fileEntry}).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
cm.On("Exec", []string{"bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "/var/run/act/workflow/1.sh"}, mock.AnythingOfType("map[string]string"), "", "workdir").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromImageEnv", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
cm.On("UpdateFromPath", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err := sr.main()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepRunPrePost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sr := &stepRun{}
|
||||
|
||||
err := sr.pre()(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = sr.post()(ctx)
|
||||
assert.Nil(t, err)
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestMergeIntoMap(t *testing.T) {
|
||||
table := []struct {
|
||||
name string
|
||||
target map[string]string
|
||||
maps []map[string]string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "testEmptyMap",
|
||||
target: map[string]string{},
|
||||
maps: []map[string]string{},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "testMergeIntoEmptyMap",
|
||||
target: map[string]string{},
|
||||
maps: []map[string]string{
|
||||
{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}, {
|
||||
"key2": "overridden",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "overridden",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "testMergeIntoExistingMap",
|
||||
target: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
maps: []map[string]string{
|
||||
{
|
||||
"key1": "overridden",
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"key1": "overridden",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mergeIntoMap(&tt.target, tt.maps...)
|
||||
assert.Equal(t, tt.expected, tt.target)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stepMock struct {
|
||||
mock.Mock
|
||||
step
|
||||
}
|
||||
|
||||
func (sm *stepMock) pre() common.Executor {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (sm *stepMock) main() common.Executor {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (sm *stepMock) post() common.Executor {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(func(context.Context) error)
|
||||
}
|
||||
|
||||
func (sm *stepMock) getRunContext() *RunContext {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(*RunContext)
|
||||
}
|
||||
|
||||
func (sm *stepMock) getStepModel() *model.Step {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(*model.Step)
|
||||
}
|
||||
|
||||
func (sm *stepMock) getEnv() *map[string]string {
|
||||
args := sm.Called()
|
||||
return args.Get(0).(*map[string]string)
|
||||
}
|
||||
|
||||
func TestSetupEnv(t *testing.T) {
|
||||
cm := &containerMock{}
|
||||
sm := &stepMock{}
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{
|
||||
Env: map[string]string{
|
||||
"GITHUB_RUN_ID": "runId",
|
||||
},
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {
|
||||
Env: yaml.Node{
|
||||
Value: "JOB_KEY: jobvalue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Env: map[string]string{
|
||||
"RC_KEY": "rcvalue",
|
||||
},
|
||||
ExtraPath: []string{"/path/to/extra/file"},
|
||||
JobContainer: cm,
|
||||
}
|
||||
step := &model.Step{
|
||||
With: map[string]string{
|
||||
"STEP_WITH": "with-value",
|
||||
},
|
||||
}
|
||||
env := map[string]string{
|
||||
"PATH": "",
|
||||
}
|
||||
|
||||
sm.On("getRunContext").Return(rc)
|
||||
sm.On("getStepModel").Return(step)
|
||||
sm.On("getEnv").Return(&env)
|
||||
|
||||
cm.On("UpdateFromImageEnv", &env).Return(func(ctx context.Context) error { return nil })
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &env).Return(func(ctx context.Context) error { return nil })
|
||||
cm.On("UpdateFromPath", &env).Return(func(ctx context.Context) error { return nil })
|
||||
|
||||
err := setupEnv(context.Background(), sm)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// These are commit or system specific
|
||||
delete((env), "GITHUB_REF")
|
||||
delete((env), "GITHUB_SHA")
|
||||
delete((env), "GITHUB_WORKSPACE")
|
||||
delete((env), "GITHUB_REPOSITORY")
|
||||
delete((env), "GITHUB_REPOSITORY_OWNER")
|
||||
delete((env), "GITHUB_ACTOR")
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"ACT": "true",
|
||||
"CI": "true",
|
||||
"GITHUB_ACTION": "",
|
||||
"GITHUB_ACTIONS": "true",
|
||||
"GITHUB_ACTION_PATH": "",
|
||||
"GITHUB_ACTION_REF": "",
|
||||
"GITHUB_ACTION_REPOSITORY": "",
|
||||
"GITHUB_API_URL": "https:///api/v3",
|
||||
"GITHUB_BASE_REF": "",
|
||||
"GITHUB_ENV": "/var/run/act/workflow/envs.txt",
|
||||
"GITHUB_EVENT_NAME": "",
|
||||
"GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json",
|
||||
"GITHUB_GRAPHQL_URL": "https:///api/graphql",
|
||||
"GITHUB_HEAD_REF": "",
|
||||
"GITHUB_JOB": "",
|
||||
"GITHUB_PATH": "/var/run/act/workflow/paths.txt",
|
||||
"GITHUB_RETENTION_DAYS": "0",
|
||||
"GITHUB_RUN_ID": "runId",
|
||||
"GITHUB_RUN_NUMBER": "1",
|
||||
"GITHUB_SERVER_URL": "https://",
|
||||
"GITHUB_TOKEN": "",
|
||||
"GITHUB_WORKFLOW": "",
|
||||
"INPUT_STEP_WITH": "with-value",
|
||||
"PATH": "/path/to/extra/file:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"RC_KEY": "rcvalue",
|
||||
"RUNNER_PERFLOG": "/dev/null",
|
||||
"RUNNER_TRACKING_ID": "",
|
||||
}, env)
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestIsStepEnabled(t *testing.T) {
|
||||
createTestStep := func(t *testing.T, input string) step {
|
||||
var step *model.Step
|
||||
err := yaml.Unmarshal([]byte(input), &step)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return &stepRun{
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
Platforms: map[string]string{
|
||||
"ubuntu-latest": "ubuntu-latest",
|
||||
},
|
||||
},
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
Env: map[string]string{},
|
||||
Run: &model.Run{
|
||||
JobID: "job1",
|
||||
Workflow: &model.Workflow{
|
||||
Name: "workflow1",
|
||||
Jobs: map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Step: step,
|
||||
}
|
||||
}
|
||||
|
||||
log.SetLevel(log.DebugLevel)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
// success()
|
||||
step := createTestStep(t, "if: success()")
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: success()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: success()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.False(isStepEnabled(context.Background(), step))
|
||||
|
||||
// failure()
|
||||
step = createTestStep(t, "if: failure()")
|
||||
assertObject.False(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: failure()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.False(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: failure()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
|
||||
// always()
|
||||
step = createTestStep(t, "if: always()")
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: always()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
|
||||
step = createTestStep(t, "if: always()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step))
|
||||
}
|
Loading…
Reference in New Issue