diff --git a/pkg/model/action.go b/pkg/model/action.go index 1cfce632791..9bf937827dc 100644 --- a/pkg/model/action.go +++ b/pkg/model/action.go @@ -49,6 +49,10 @@ type ActionRuns struct { Using ActionRunsUsing `yaml:"using"` Env map[string]string `yaml:"env"` Main string `yaml:"main"` + Pre string `yaml:"pre"` + PreIf string `yaml:"pre-if"` + Post string `yaml:"post"` + PostIf string `yaml:"post-if"` Image string `yaml:"image"` Entrypoint string `yaml:"entrypoint"` Args []string `yaml:"args"` @@ -90,5 +94,13 @@ func ReadAction(in io.Reader) (*Action, error) { return nil, err } + // set defaults + if a.Runs.Pre != "" && a.Runs.PreIf == "" { + a.Runs.PreIf = "always()" + } + if a.Runs.Post != "" && a.Runs.PostIf == "" { + a.Runs.PostIf = "always()" + } + return a, nil } diff --git a/pkg/model/step_result.go b/pkg/model/step_result.go index 86e5ebf38cd..49b7705d4ce 100644 --- a/pkg/model/step_result.go +++ b/pkg/model/step_result.go @@ -42,4 +42,5 @@ type StepResult struct { Outputs map[string]string `json:"outputs"` Conclusion stepStatus `json:"conclusion"` Outcome stepStatus `json:"outcome"` + State map[string]string } diff --git a/pkg/runner/action.go b/pkg/runner/action.go index 510e9473d21..48069fe91f5 100644 --- a/pkg/runner/action.go +++ b/pkg/runner/action.go @@ -127,6 +127,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction // time, we don't have all environment prepared mergeIntoMap(step.getEnv(), rc.withGithubEnv(map[string]string{})) + populateEnvsFromSavedState(step.getEnv(), step, rc) populateEnvsFromInput(step.getEnv(), action, rc) actionLocation := path.Join(actionDir, actionPath) @@ -497,6 +498,16 @@ func (rc *RunContext) compositeExecutor(action *model.Action) common.Executor { } } +func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) { + stepResult := rc.StepResults[step.getStepModel().ID] + if stepResult != nil { + for name, value := range stepResult.State { + envName := fmt.Sprintf("STATE_%s", name) + (*env)[envName] = value + } + } +} + func populateEnvsFromInput(env *map[string]string, action *model.Action, rc *RunContext) { eval := rc.NewExpressionEvaluator() for inputID, input := range action.Inputs { @@ -538,3 +549,68 @@ func getOsSafeRelativePath(s, prefix string) string { return actionName } + +func shouldRunPostStep(step actionStep) common.Conditional { + return func(ctx context.Context) bool { + stepResults := step.getRunContext().getStepsContext() + + if stepResults[step.getStepModel().ID].Conclusion == model.StepStatusSkipped { + return false + } + + if step.getActionModel() == nil { + return false + } + + return true + } +} + +func hasPostStep(step actionStep) common.Conditional { + return func(ctx context.Context) bool { + action := step.getActionModel() + return (action.Runs.Using == model.ActionRunsUsingNode12 || + action.Runs.Using == model.ActionRunsUsingNode16) && + action.Runs.Post != "" + } +} + +func runPostStep(step actionStep) common.Executor { + return func(ctx context.Context) error { + rc := step.getRunContext() + stepModel := step.getStepModel() + action := step.getActionModel() + + switch action.Runs.Using { + case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16: + + populateEnvsFromSavedState(step.getEnv(), step, rc) + + var actionDir string + var actionPath string + if _, ok := step.(*stepActionRemote); ok { + actionPath = newRemoteAction(stepModel.Uses).Path + actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(stepModel.Uses, "/", "-")) + } else { + actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses) + actionPath = "" + } + + actionLocation := "" + if actionPath != "" { + actionLocation = path.Join(actionDir, actionPath) + } else { + actionLocation = actionDir + } + + _, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc) + + containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)} + log.Debugf("executing remote job container: %s", containerArgs) + + return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) + default: + return nil + } + } +} diff --git a/pkg/runner/action_test.go b/pkg/runner/action_test.go index 7e1c1297b17..e1400a79f68 100644 --- a/pkg/runner/action_test.go +++ b/pkg/runner/action_test.go @@ -138,11 +138,12 @@ runs: func TestActionRunner(t *testing.T) { table := []struct { - name string - step actionStep + name string + step actionStep + expectedEnv map[string]string }{ { - name: "Test", + name: "with-input", step: &stepActionRemote{ Step: &model.Step{ Uses: "repo@ref", @@ -172,6 +173,47 @@ func TestActionRunner(t *testing.T) { }, env: map[string]string{}, }, + expectedEnv: map[string]string{"INPUT_KEY": "default value"}, + }, + { + name: "restore-saved-state", + step: &stepActionRemote{ + Step: &model.Step{ + ID: "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", + }, + }, + }, + }, + CurrentStep: "post-step", + StepResults: map[string]*model.StepResult{ + "step": { + State: map[string]string{ + "name": "state value", + }, + }, + }, + }, + action: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + }, + }, + env: map[string]string{}, + }, + expectedEnv: map[string]string{"STATE_name": "state value"}, }, } @@ -183,8 +225,14 @@ func TestActionRunner(t *testing.T) { cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil }) envMatcher := mock.MatchedBy(func(env map[string]string) bool { - return env["INPUT_KEY"] == "default value" + for k, v := range tt.expectedEnv { + if env[k] != v { + return false + } + } + return true }) + cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, envMatcher, "", "").Return(func(ctx context.Context) error { return nil }) tt.step.getRunContext().JobContainer = cm diff --git a/pkg/runner/command.go b/pkg/runner/command.go index 3182f83a195..ce77bdf9518 100755 --- a/pkg/runner/command.go +++ b/pkg/runner/command.go @@ -63,6 +63,9 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { case resumeCommand: resumeCommand = "" logger.Infof(" \U00002699 %s", line) + case "save-state": + logger.Infof(" \U0001f4be %s", line) + rc.saveState(ctx, kvPairs, arg) default: logger.Infof(" \U00002753 %s", line) } @@ -141,3 +144,15 @@ func unescapeKvPairs(kvPairs map[string]string) map[string]string { } return kvPairs } + +func (rc *RunContext) saveState(ctx context.Context, kvPairs map[string]string, arg string) { + if rc.CurrentStep != "" { + stepResult := rc.StepResults[rc.CurrentStep] + if stepResult != nil { + if stepResult.State == nil { + stepResult.State = map[string]string{} + } + stepResult.State[kvPairs["name"]] = arg + } + } +} diff --git a/pkg/runner/command_test.go b/pkg/runner/command_test.go index bd7bbdc75f9..1c72768eef4 100644 --- a/pkg/runner/command_test.go +++ b/pkg/runner/command_test.go @@ -173,3 +173,21 @@ func TestAddmaskUsemask(t *testing.T) { a.Equal("[testjob] \U00002699 ***\n[testjob] \U00002699 ::set-output:: = token=***\n", re) } + +func TestSaveState(t *testing.T) { + rc := &RunContext{ + CurrentStep: "step", + StepResults: map[string]*model.StepResult{ + "step": { + State: map[string]string{}, + }, + }, + } + + ctx := context.Background() + + handler := rc.commandHandler(ctx) + handler("::save-state name=state-name::state-value\n") + + assert.Equal(t, "state-value", rc.StepResults["step"].State["state-name"]) +} diff --git a/pkg/runner/step.go b/pkg/runner/step.go index b6ed65a45ea..683759b9b70 100644 --- a/pkg/runner/step.go +++ b/pkg/runner/step.go @@ -18,14 +18,49 @@ type step interface { getRunContext() *RunContext getStepModel() *model.Step getEnv() *map[string]string + getIfExpression(stage stepStage) string } -func runStepExecutor(step step, executor common.Executor) common.Executor { +type stepStage int + +const ( + stepStagePre stepStage = iota + stepStageMain + stepStagePost +) + +func (s stepStage) String() string { + switch s { + case stepStagePre: + return "Pre" + case stepStageMain: + return "Run" + case stepStagePost: + return "Post" + } + return "Unknown" +} + +func (s stepStage) getStepName(stepModel *model.Step) string { + switch s { + case stepStagePre: + return fmt.Sprintf("pre-%s", stepModel.ID) + case stepStageMain: + return stepModel.ID + case stepStagePost: + return fmt.Sprintf("post-%s", stepModel.ID) + } + return "unknown" +} + +func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor { return func(ctx context.Context) error { rc := step.getRunContext() stepModel := step.getStepModel() - rc.CurrentStep = stepModel.ID + ifExpression := step.getIfExpression(stage) + rc.CurrentStep = stage.getStepName(stepModel) + rc.StepResults[rc.CurrentStep] = &model.StepResult{ Outcome: model.StepStatusSuccess, Conclusion: model.StepStatusSuccess, @@ -37,7 +72,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor { return err } - runStep, err := isStepEnabled(ctx, step) + runStep, err := isStepEnabled(ctx, ifExpression, step) if err != nil { rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure @@ -45,7 +80,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor { } if !runStep { - log.Debugf("Skipping step '%s' due to '%s'", stepModel.String(), stepModel.If.Value) + log.Debugf("Skipping step '%s' due to '%s'", stepModel, stepModel.If.Value) rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped return nil @@ -55,7 +90,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor { if strings.Contains(stepString, "::add-mask::") { stepString = "add-mask command" } - common.Logger(ctx).Infof("\u2B50 Run %s", stepString) + common.Logger(ctx).Infof("\u2B50 %s %s", stage, stepString) err = executor(ctx) @@ -129,10 +164,10 @@ func mergeEnv(step step) { mergeIntoMap(env, rc.withGithubEnv(*env)) } -func isStepEnabled(ctx context.Context, step step) (bool, error) { +func isStepEnabled(ctx context.Context, expr string, step step) (bool, error) { rc := step.getRunContext() - runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), step.getStepModel().If.Value) + runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), expr) if err != nil { return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", step.getStepModel().If.Value, err) } diff --git a/pkg/runner/step_action_local.go b/pkg/runner/step_action_local.go index de3c9cc39e8..5f9c1b19278 100644 --- a/pkg/runner/step_action_local.go +++ b/pkg/runner/step_action_local.go @@ -32,7 +32,7 @@ func (sal *stepActionLocal) pre() common.Executor { func (sal *stepActionLocal) main() common.Executor { sal.env = map[string]string{} - return runStepExecutor(sal, func(ctx context.Context) error { + return runStepExecutor(sal, stepStageMain, func(ctx context.Context) error { actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses) localReader := func(ctx context.Context) actionYamlReader { @@ -63,9 +63,7 @@ func (sal *stepActionLocal) main() common.Executor { } func (sal *stepActionLocal) post() common.Executor { - return func(ctx context.Context) error { - return nil - } + return runStepExecutor(sal, stepStagePost, runPostStep(sal)).If(hasPostStep(sal)).If(shouldRunPostStep(sal)) } func (sal *stepActionLocal) getRunContext() *RunContext { @@ -80,6 +78,16 @@ func (sal *stepActionLocal) getEnv() *map[string]string { return &sal.env } +func (sal *stepActionLocal) getIfExpression(stage stepStage) string { + switch stage { + case stepStageMain: + return sal.Step.If.Value + case stepStagePost: + return sal.action.Runs.PostIf + } + return "" +} + func (sal *stepActionLocal) getActionModel() *model.Action { return sal.action } diff --git a/pkg/runner/step_action_local_test.go b/pkg/runner/step_action_local_test.go index 58607299b2d..055169b8608 100644 --- a/pkg/runner/step_action_local_test.go +++ b/pkg/runner/step_action_local_test.go @@ -2,12 +2,14 @@ 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" + "gopkg.in/yaml.v3" ) type stepActionLocalMocks struct { @@ -88,14 +90,205 @@ func TestStepActionLocalTest(t *testing.T) { salm.AssertExpectations(t) } -func TestStepActionLocalPrePost(t *testing.T) { +func TestStepActionLocalPre(t *testing.T) { ctx := context.Background() sal := &stepActionLocal{} err := sal.pre()(ctx) assert.Nil(t, err) +} - err = sal.post()(ctx) - assert.Nil(t, err) +func TestStepActionLocalPost(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + actionModel *model.Action + initialStepResults map[string]*model.StepResult + expectedPostStepResult *model.StepResult + err error + mocks struct { + env bool + exec bool + } + }{ + { + name: "main-success", + stepModel: &model.Step{ + ID: "step", + Uses: "./local/action", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "main-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "./local/action", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "skip-if-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "./local/action", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "success()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: false, + }, + }, + { + name: "skip-if-main-skipped", + stepModel: &model.Step{ + ID: "step", + If: yaml.Node{Value: "failure()"}, + Uses: "./local/action", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: nil, + mocks: struct { + env bool + exec bool + }{ + env: false, + exec: false, + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + cm := &containerMock{} + + sal := &stepActionLocal{ + env: map[string]string{}, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://github.com", + }, + JobContainer: cm, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + StepResults: tt.initialStepResults, + }, + Step: tt.stepModel, + action: tt.actionModel, + } + + if tt.mocks.env { + cm.On("UpdateFromImageEnv", &sal.env).Return(func(ctx context.Context) error { return nil }) + cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", &sal.env).Return(func(ctx context.Context) error { return nil }) + cm.On("UpdateFromPath", &sal.env).Return(func(ctx context.Context) error { return nil }) + } + if tt.mocks.exec { + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(array []string) bool { + return strings.HasSuffix(array[1], suffix) + }) + } + cm.On("Exec", suffixMatcher("pkg/runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err }) + } + + err := sal.post()(ctx) + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.expectedPostStepResult, sal.RunContext.StepResults["post-step"]) + cm.AssertExpectations(t) + }) + } } diff --git a/pkg/runner/step_action_remote.go b/pkg/runner/step_action_remote.go index f1b532a817a..858693f1f11 100644 --- a/pkg/runner/step_action_remote.go +++ b/pkg/runner/step_action_remote.go @@ -38,7 +38,7 @@ var ( func (sar *stepActionRemote) main() common.Executor { sar.env = map[string]string{} - return runStepExecutor(sar, func(ctx context.Context) error { + return runStepExecutor(sar, stepStageMain, 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) @@ -92,9 +92,7 @@ func (sar *stepActionRemote) main() common.Executor { } func (sar *stepActionRemote) post() common.Executor { - return func(ctx context.Context) error { - return nil - } + return runStepExecutor(sar, stepStagePost, runPostStep(sar)).If(hasPostStep(sar)).If(shouldRunPostStep(sar)) } func (sar *stepActionRemote) getRunContext() *RunContext { @@ -109,6 +107,18 @@ func (sar *stepActionRemote) getEnv() *map[string]string { return &sar.env } +func (sar *stepActionRemote) getIfExpression(stage stepStage) string { + switch stage { + case stepStagePre: + return sar.action.Runs.PreIf + case stepStageMain: + return sar.Step.If.Value + case stepStagePost: + return sar.action.Runs.PostIf + } + return "" +} + func (sar *stepActionRemote) getActionModel() *model.Action { return sar.action } diff --git a/pkg/runner/step_action_remote_test.go b/pkg/runner/step_action_remote_test.go index fd5ebcd8b93..4425268069e 100644 --- a/pkg/runner/step_action_remote_test.go +++ b/pkg/runner/step_action_remote_test.go @@ -2,6 +2,7 @@ package runner import ( "context" + "errors" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/nektos/act/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "gopkg.in/yaml.v3" ) type stepActionRemoteMocks struct { @@ -25,78 +27,369 @@ func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string, return args.Get(0).(func(context.Context) error) } -func TestStepActionRemoteTest(t *testing.T) { - ctx := context.Background() +func TestStepActionRemote(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + result *model.StepResult + mocks struct { + env bool + cloned bool + read bool + run bool + } + runError error + }{ + { + name: "run-successful", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + result: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: true, + }, + }, + { + name: "run-skipped", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + If: yaml.Node{Value: "false"}, + }, + result: &model.StepResult{ + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: false, + read: false, + run: false, + }, + }, + { + name: "run-error", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + result: &model.StepResult{ + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: true, + }, + runError: errors.New("error"), + }, + } - cm := &containerMock{} + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() - sarm := &stepActionRemoteMocks{} + cm := &containerMock{} + sarm := &stepActionRemoteMocks{} - clonedAction := false + 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: "github.com", - }, - Run: &model.Run{ - JobID: "1", - Workflow: &model.Workflow{ - Jobs: map[string]*model.Job{ - "1": {}, + 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: "github.com", + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, }, + StepResults: map[string]*model.StepResult{}, + JobContainer: cm, }, - }, - StepResults: map[string]*model.StepResult{}, - JobContainer: cm, - }, - Step: &model.Step{ - Uses: "remote/action@v1", - }, - readAction: sarm.readAction, - runAction: sarm.runAction, - } + Step: tt.stepModel, + readAction: sarm.readAction, + runAction: sarm.runAction, + } - suffixMatcher := func(suffix string) interface{} { - return mock.MatchedBy(func(actionDir string) bool { - return strings.HasSuffix(actionDir, suffix) - }) - } + 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 }) + if tt.mocks.env { + 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 }) + } + if tt.mocks.read { + sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + } + if tt.mocks.run { + sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError }) + } - 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"), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return nil }) + err := sar.main()(ctx) - err := sar.main()(ctx) + assert.Equal(t, tt.runError, err) + assert.Equal(t, tt.mocks.cloned, clonedAction) + assert.Equal(t, tt.result, sar.RunContext.StepResults["step"]) - assert.Nil(t, err) - assert.True(t, clonedAction) - sarm.AssertExpectations(t) - cm.AssertExpectations(t) + sarm.AssertExpectations(t) + cm.AssertExpectations(t) + }) + } } -func TestStepActionRemotePrePost(t *testing.T) { +func TestStepActionRemotePre(t *testing.T) { ctx := context.Background() sar := &stepActionRemote{} err := sar.pre()(ctx) assert.Nil(t, err) +} - err = sar.post()(ctx) - assert.Nil(t, err) +func TestStepActionRemotePost(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + actionModel *model.Action + initialStepResults map[string]*model.StepResult + expectedEnv map[string]string + expectedPostStepResult *model.StepResult + err error + mocks struct { + env bool + exec bool + } + }{ + { + name: "main-success", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + State: map[string]string{ + "key": "value", + }, + }, + }, + expectedEnv: map[string]string{ + "STATE_key": "value", + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "main-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "skip-if-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "success()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: &model.StepResult{ + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: false, + }, + }, + { + name: "skip-if-main-skipped", + stepModel: &model.Step{ + ID: "step", + If: yaml.Node{Value: "failure()"}, + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + }, + expectedPostStepResult: nil, + mocks: struct { + env bool + exec bool + }{ + env: false, + exec: false, + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + cm := &containerMock{} + + sar := &stepActionRemote{ + env: map[string]string{}, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://github.com", + }, + JobContainer: cm, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + StepResults: tt.initialStepResults, + }, + Step: tt.stepModel, + action: tt.actionModel, + } + + if tt.mocks.env { + 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 }) + } + if tt.mocks.exec { + cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err }) + } + + err := sar.post()(ctx) + + assert.Equal(t, tt.err, err) + if tt.expectedEnv != nil { + for key, value := range tt.expectedEnv { + assert.Equal(t, value, sar.env[key]) + } + } + assert.Equal(t, tt.expectedPostStepResult, sar.RunContext.StepResults["post-step"]) + cm.AssertExpectations(t) + }) + } } diff --git a/pkg/runner/step_docker.go b/pkg/runner/step_docker.go index 3a274d0fc8f..e3bda180439 100644 --- a/pkg/runner/step_docker.go +++ b/pkg/runner/step_docker.go @@ -26,7 +26,7 @@ func (sd *stepDocker) pre() common.Executor { func (sd *stepDocker) main() common.Executor { sd.env = map[string]string{} - return runStepExecutor(sd, sd.runUsesContainer()) + return runStepExecutor(sd, stepStageMain, sd.runUsesContainer()) } func (sd *stepDocker) post() common.Executor { @@ -47,6 +47,10 @@ func (sd *stepDocker) getEnv() *map[string]string { return &sd.env } +func (sd *stepDocker) getIfExpression(stage stepStage) string { + return sd.Step.If.Value +} + func (sd *stepDocker) runUsesContainer() common.Executor { rc := sd.RunContext step := sd.Step diff --git a/pkg/runner/step_run.go b/pkg/runner/step_run.go index a26b31a544d..9a6fe34e719 100644 --- a/pkg/runner/step_run.go +++ b/pkg/runner/step_run.go @@ -27,7 +27,7 @@ func (sr *stepRun) pre() common.Executor { func (sr *stepRun) main() common.Executor { sr.env = map[string]string{} - return runStepExecutor(sr, common.NewPipelineExecutor( + return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor( sr.setupShellCommandExecutor(), func(ctx context.Context) error { return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.Step.WorkingDirectory)(ctx) @@ -53,6 +53,10 @@ func (sr *stepRun) getEnv() *map[string]string { return &sr.env } +func (sr *stepRun) getIfExpression(stage stepStage) string { + return sr.Step.If.Value +} + func (sr *stepRun) setupShellCommandExecutor() common.Executor { return func(ctx context.Context) error { scriptName, script, err := sr.setupShellCommand(ctx) diff --git a/pkg/runner/step_test.go b/pkg/runner/step_test.go index b87efeb0049..4f4b3a4d5a0 100644 --- a/pkg/runner/step_test.go +++ b/pkg/runner/step_test.go @@ -230,49 +230,49 @@ func TestIsStepEnabled(t *testing.T) { // success() step := createTestStep(t, "if: success()") - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: success()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusSuccess, } - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: success()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusFailure, } - assertObject.False(isStepEnabled(context.Background(), step)) + assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) // failure() step = createTestStep(t, "if: failure()") - assertObject.False(isStepEnabled(context.Background(), step)) + assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: failure()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusSuccess, } - assertObject.False(isStepEnabled(context.Background(), step)) + assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: failure()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusFailure, } - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) // always() step = createTestStep(t, "if: always()") - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: always()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusSuccess, } - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) step = createTestStep(t, "if: always()") step.getRunContext().StepResults["a"] = &model.StepResult{ Conclusion: model.StepStatusFailure, } - assertObject.True(isStepEnabled(context.Background(), step)) + assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step)) }