implement pre and post steps (#1089)

* feat: add post step to actions and add state command

This commit includes requried changes for running post steps
for local and remote actions.
This allows general cleanup work to be done after executing
an action.

Communication is allowed between this steps, by using the
action state.

* feat: collect pre and post steps for composite actions

* refactor: move composite action logic into own file

* refactor: restructure composite handling

* feat: run composite post steps during post step lifecycle

* refactor: remove duplicate log output

* feat: run all composite post actions in a step

Since composite actions could have multiple pre/post steps inside,
we need to run all of them in a single top-level pre/post step.

This PR includes a test case for this and the correct order of steps
to be executed.

* refactor: remove unused lines of code

* refactor: simplify test expression

* fix: use composite job logger

* fix: make step output more readable

* fix: enforce running all post executor

To make sure every post executor/step is executed, it is chained
with it's own Finally executor.

* fix: do not run post step if no step result is available

Having no step result means we do not run any step (neither pre
nor main) and we do not need to run post.

* fix: setup defaults

If no pre-if or post-if is given, it should default to 'always()'.
This could be set even if there is no pre or post step.
In fact this is required for composite actions and included post
steps to run.

* fix: output step related if expression

* test: update expectation

* feat: run pre step from actions (#1110)

This PR implements running pre steps for remote actions.
This includes remote actions using inside local composite actions.

* fix: set correct expr default status checks

For post-if conditions the default status check should be
always(), while for all other if expression the default status
check is success()

References:
https://docs.github.com/en/actions/learn-github-actions/expressions#status-check-functions
https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspost-if

* fix: remove code added during rebase
This commit is contained in:
Markus Wolf 2022-05-24 15:36:06 +02:00 committed by GitHub
parent ebb408f373
commit 943a0e6eea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1560 additions and 350 deletions

View File

@ -37,7 +37,7 @@ func TestFunctionContains(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -66,7 +66,7 @@ func TestFunctionStartsWith(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -95,7 +95,7 @@ func TestFunctionEndsWith(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -122,7 +122,7 @@ func TestFunctionJoin(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -148,7 +148,7 @@ func TestFunctionToJSON(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -171,7 +171,7 @@ func TestFunctionFromJSON(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -197,7 +197,7 @@ func TestFunctionHashFiles(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
workdir, err := filepath.Abs("testdata")
assert.Nil(t, err)
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -234,7 +234,7 @@ func TestFunctionFormat(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
if tt.error != nil {
assert.Equal(t, tt.error, err.Error())
} else {

View File

@ -30,8 +30,32 @@ type Config struct {
Context string
}
type DefaultStatusCheck int
const (
DefaultStatusCheckNone DefaultStatusCheck = iota
DefaultStatusCheckSuccess
DefaultStatusCheckAlways
DefaultStatusCheckCanceled
DefaultStatusCheckFailure
)
func (dsc DefaultStatusCheck) String() string {
switch dsc {
case DefaultStatusCheckSuccess:
return "success"
case DefaultStatusCheckAlways:
return "always"
case DefaultStatusCheckCanceled:
return "cancelled"
case DefaultStatusCheckFailure:
return "failure"
}
return ""
}
type Interpreter interface {
Evaluate(input string, isIfExpression bool) (interface{}, error)
Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (interface{}, error)
}
type interperterImpl struct {
@ -46,9 +70,9 @@ func NewInterpeter(env *EvaluationEnvironment, config Config) Interpreter {
}
}
func (impl *interperterImpl) Evaluate(input string, isIfExpression bool) (interface{}, error) {
func (impl *interperterImpl) Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (interface{}, error) {
input = strings.TrimPrefix(input, "${{")
if isIfExpression && input == "" {
if defaultStatusCheck != DefaultStatusCheckNone && input == "" {
input = "success()"
}
parser := actionlint.NewExprParser()
@ -57,7 +81,7 @@ func (impl *interperterImpl) Evaluate(input string, isIfExpression bool) (interf
return nil, fmt.Errorf("Failed to parse: %s", err.Message)
}
if isIfExpression {
if defaultStatusCheck != DefaultStatusCheckNone {
hasStatusCheckFunction := false
actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
@ -72,7 +96,7 @@ func (impl *interperterImpl) Evaluate(input string, isIfExpression bool) (interf
exprNode = &actionlint.LogicalOpNode{
Kind: actionlint.LogicalOpNodeKindAnd,
Left: &actionlint.FuncCallNode{
Callee: "success",
Callee: defaultStatusCheck.String(),
Args: []actionlint.ExprNode{},
},
Right: exprNode,
@ -361,7 +385,7 @@ func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value {
}
// try to parse the string as a number
evaluated, err := impl.Evaluate(value.String(), false)
evaluated, err := impl.Evaluate(value.String(), DefaultStatusCheckNone)
if err != nil {
return reflect.ValueOf(math.NaN())
}

View File

@ -29,7 +29,7 @@ func TestLiterals(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -93,7 +93,7 @@ func TestOperators(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
if tt.error != "" {
assert.NotNil(t, err)
assert.Equal(t, tt.error, err.Error())
@ -146,7 +146,7 @@ func TestOperatorsCompare(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)
@ -509,7 +509,7 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
@ -607,7 +607,7 @@ func TestContexts(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err)
assert.Equal(t, tt.expected, output)

View File

@ -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.PreIf == "" {
a.Runs.PreIf = "always()"
}
if a.Runs.PostIf == "" {
a.Runs.PostIf = "always()"
}
return a, nil
}

View File

@ -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
}

View File

@ -24,6 +24,8 @@ type actionStep interface {
step
getActionModel() *model.Action
getCompositeRunContext() *RunContext
getCompositeSteps() *compositeSteps
}
type readAction func(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
@ -98,14 +100,37 @@ func readActionImpl(step *model.Step, actionDir string, actionPath string, readF
return action, err
}
func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir string, actionPath string, containerActionDir string) error {
rc := step.getRunContext()
stepModel := step.getStepModel()
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)
}
func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor {
rc := step.getRunContext()
stepModel := step.getStepModel()
return func(ctx context.Context) error {
actionPath := ""
if remoteAction != nil && remoteAction.Path != "" {
actionPath = remoteAction.Path
}
action := step.getActionModel()
log.Debugf("About to run action %v", action)
@ -127,6 +152,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)
@ -134,27 +160,9 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
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 {
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 {
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
return err
}
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
@ -167,10 +175,11 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
}
return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
case model.ActionRunsUsingComposite:
if err := maybeCopyToActionDir(); err != nil {
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
return err
}
return execAsComposite(step, containerActionDir)(ctx)
return execAsComposite(step)(ctx)
default:
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
model.ActionRunsUsingDocker,
@ -359,134 +368,36 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
return stepContainer
}
func execAsComposite(step actionStep, containerActionDir string) common.Executor {
rc := step.getRunContext()
func (rc *RunContext) setupActionInputs(step actionStep) {
if step.getActionModel() == nil {
// e.g. local checkout skip has no action model
return
}
stepModel := step.getStepModel()
action := step.getActionModel()
return func(ctx context.Context) error {
eval := rc.NewExpressionEvaluator()
inputs := make(map[string]interface{})
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)
}
}
env := make(map[string]string)
for k, v := range rc.Env {
env[k] = eval.Interpolate(v)
}
for k, v := range step.getStepModel().Environment() {
env[k] = eval.Interpolate(v)
}
// run with the global config but without secrets
configCopy := *rc.Config
configCopy.Secrets = nil
// create a run context for the composite action to run in
compositerc := &RunContext{
Name: rc.Name,
JobName: rc.JobName,
Run: &model.Run{
JobID: "composite-job",
Workflow: &model.Workflow{
Name: rc.Run.Workflow.Name,
Jobs: map[string]*model.Job{
"composite-job": {},
},
},
},
Config: &configCopy,
StepResults: map[string]*model.StepResult{},
JobContainer: rc.JobContainer,
Inputs: inputs,
ActionPath: containerActionDir,
ActionRepository: rc.ActionRepository,
ActionRef: rc.ActionRef,
Env: env,
Masks: rc.Masks,
ExtraPath: rc.ExtraPath,
}
ctx = WithCompositeLogger(ctx, &compositerc.Masks)
// We need to inject a composite RunContext related command
// handler into the current running job container
// We need this, to support scoping commands to the composite action
// executing.
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(compositerc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
oldout, olderr := compositerc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
defer (func() {
rc.JobContainer.ReplaceLogWriter(oldout, olderr)
})()
err := compositerc.compositeExecutor(action)(ctx)
// Map outputs from composite RunContext to job RunContext
eval = compositerc.NewExpressionEvaluator()
for outputName, output := range action.Outputs {
rc.setOutput(ctx, map[string]string{
"name": outputName,
}, eval.Interpolate(output.Value))
}
rc.Masks = compositerc.Masks
rc.ExtraPath = compositerc.ExtraPath
return err
eval := rc.NewExpressionEvaluator()
inputs := make(map[string]interface{})
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
}
// Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) compositeExecutor(action *model.Action) common.Executor {
steps := make([]common.Executor, 0)
sf := &stepFactoryImpl{}
for i, step := range action.Runs.Steps {
if step.ID == "" {
step.ID = fmt.Sprintf("%d", i)
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
}
// create a copy of the step, since this composite action could
// run multiple times and we might modify the instance
stepcopy := step
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 {
common.Logger(ctx).Errorf("%v", err)
common.SetJobError(ctx, err)
} else if ctx.Err() != nil {
common.Logger(ctx).Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
}
return nil
})
}
steps = append(steps, common.JobError)
return func(ctx context.Context) error {
return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
}
}
@ -531,3 +442,162 @@ func getOsSafeRelativePath(s, prefix string) string {
return actionName
}
func shouldRunPreStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
log := common.Logger(ctx)
if step.getActionModel() == nil {
log.Debugf("skip pre step for '%s': no action model available", step.getStepModel())
return false
}
return true
}
}
func hasPreStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
action := step.getActionModel()
return action.Runs.Using == model.ActionRunsUsingComposite ||
((action.Runs.Using == model.ActionRunsUsingNode12 ||
action.Runs.Using == model.ActionRunsUsingNode16) &&
action.Runs.Pre != "")
}
}
func runPreStep(step actionStep) common.Executor {
return func(ctx context.Context) error {
common.Logger(ctx).Debugf("run pre step for '%s'", step.getStepModel())
rc := step.getRunContext()
stepModel := step.getStepModel()
action := step.getActionModel()
switch action.Runs.Using {
case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16:
// todo: refactor into step
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)
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
return err
}
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
log.Debugf("executing remote job container: %s", containerArgs)
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
case model.ActionRunsUsingComposite:
step.getCompositeRunContext().updateCompositeRunContext(step.getRunContext(), step)
return step.getCompositeSteps().pre(ctx)
default:
return nil
}
}
}
func shouldRunPostStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
log := common.Logger(ctx)
stepResults := step.getRunContext().getStepsContext()
stepResult := stepResults[step.getStepModel().ID]
if stepResult == nil {
log.Debugf("skip post step for '%s'; step was not executed", step.getStepModel())
return false
}
if stepResult.Conclusion == model.StepStatusSkipped {
log.Debugf("skip post step for '%s'; main step was skipped", step.getStepModel())
return false
}
if step.getActionModel() == nil {
log.Debugf("skip post step for '%s': no action model available", step.getStepModel())
return false
}
return true
}
}
func hasPostStep(step actionStep) common.Conditional {
return func(ctx context.Context) bool {
action := step.getActionModel()
return action.Runs.Using == model.ActionRunsUsingComposite ||
((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 {
common.Logger(ctx).Debugf("run post step for '%s'", step.getStepModel())
rc := step.getRunContext()
stepModel := step.getStepModel()
action := step.getActionModel()
// todo: refactor into step
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)
switch action.Runs.Using {
case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16:
populateEnvsFromSavedState(step.getEnv(), step, 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)
case model.ActionRunsUsingComposite:
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
return err
}
step.getCompositeRunContext().updateCompositeRunContext(step.getRunContext(), step)
return step.getCompositeSteps().post(ctx)
default:
return nil
}
}
}

View File

@ -0,0 +1,195 @@
package runner
import (
"context"
"fmt"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
)
func evaluteCompositeInputAndEnv(parent *RunContext, step actionStep) (inputs map[string]interface{}, env map[string]string) {
eval := parent.NewExpressionEvaluator()
inputs = make(map[string]interface{})
for k, input := range step.getActionModel().Inputs {
inputs[k] = eval.Interpolate(input.Default)
}
if step.getStepModel().With != nil {
for k, v := range step.getStepModel().With {
inputs[k] = eval.Interpolate(v)
}
}
env = make(map[string]string)
for k, v := range parent.Env {
env[k] = eval.Interpolate(v)
}
for k, v := range step.getStepModel().Environment() {
env[k] = eval.Interpolate(v)
}
return inputs, env
}
func newCompositeRunContext(parent *RunContext, step actionStep, actionPath string) *RunContext {
inputs, env := evaluteCompositeInputAndEnv(parent, step)
// run with the global config but without secrets
configCopy := *(parent.Config)
configCopy.Secrets = nil
// create a run context for the composite action to run in
compositerc := &RunContext{
Name: parent.Name,
JobName: parent.JobName,
Run: &model.Run{
JobID: "composite-job",
Workflow: &model.Workflow{
Name: parent.Run.Workflow.Name,
Jobs: map[string]*model.Job{
"composite-job": {},
},
},
},
Config: &configCopy,
StepResults: map[string]*model.StepResult{},
JobContainer: parent.JobContainer,
Inputs: inputs,
ActionPath: actionPath,
ActionRepository: parent.ActionRepository,
ActionRef: parent.ActionRef,
Env: env,
Masks: parent.Masks,
ExtraPath: parent.ExtraPath,
}
return compositerc
}
// This updates a composite context inputs, env and masks.
// This is needed to re-evalute/update that context between pre/main/post steps.
// Some of the inputs/env may requires the results of in-between steps.
func (rc *RunContext) updateCompositeRunContext(parent *RunContext, step actionStep) {
inputs, env := evaluteCompositeInputAndEnv(parent, step)
rc.Inputs = inputs
rc.Env = env
rc.Masks = append(rc.Masks, parent.Masks...)
}
func execAsComposite(step actionStep) common.Executor {
rc := step.getRunContext()
action := step.getActionModel()
return func(ctx context.Context) error {
compositerc := step.getCompositeRunContext()
steps := step.getCompositeSteps()
ctx = WithCompositeLogger(ctx, &compositerc.Masks)
compositerc.updateCompositeRunContext(rc, step)
err := steps.main(ctx)
// Map outputs from composite RunContext to job RunContext
eval := compositerc.NewExpressionEvaluator()
for outputName, output := range action.Outputs {
rc.setOutput(ctx, map[string]string{
"name": outputName,
}, eval.Interpolate(output.Value))
}
rc.Masks = append(rc.Masks, compositerc.Masks...)
rc.ExtraPath = compositerc.ExtraPath
return err
}
}
type compositeSteps struct {
pre common.Executor
main common.Executor
post common.Executor
}
// Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
steps := make([]common.Executor, 0)
preSteps := make([]common.Executor, 0)
var postExecutor common.Executor
sf := &stepFactoryImpl{}
for i, step := range action.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
step, err := sf.newStep(&stepcopy, rc)
if err != nil {
return &compositeSteps{
main: common.NewErrorExecutor(err),
}
}
preSteps = append(preSteps, step.pre())
steps = append(steps, func(ctx context.Context) error {
err := step.main()(ctx)
if err != nil {
common.Logger(ctx).Errorf("%v", err)
common.SetJobError(ctx, err)
} else if ctx.Err() != nil {
common.Logger(ctx).Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
}
return nil
})
// run the post executor in reverse order
if postExecutor != nil {
postExecutor = step.post().Finally(postExecutor)
} else {
postExecutor = step.post()
}
}
steps = append(steps, common.JobError)
return &compositeSteps{
pre: rc.newCompositeCommandExecutor(common.NewPipelineExecutor(preSteps...)),
main: rc.newCompositeCommandExecutor(func(ctx context.Context) error {
return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
}),
post: rc.newCompositeCommandExecutor(postExecutor),
}
}
func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor {
return func(ctx context.Context) error {
ctx = WithCompositeLogger(ctx, &rc.Masks)
// We need to inject a composite RunContext related command
// handler into the current running job container
// We need this, to support scoping commands to the composite action
// executing.
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
})
oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
defer rc.JobContainer.ReplaceLogWriter(oldout, olderr)
return executor(ctx)
}
}

View File

@ -44,8 +44,10 @@ runs:
expected: &model.Action{
Name: "name",
Runs: model.ActionRuns{
Using: "node16",
Main: "main.js",
Using: "node16",
Main: "main.js",
PreIf: "always()",
PostIf: "always()",
},
},
},
@ -57,8 +59,10 @@ runs:
expected: &model.Action{
Name: "name",
Runs: model.ActionRuns{
Using: "node16",
Main: "main.js",
Using: "node16",
Main: "main.js",
PreIf: "always()",
PostIf: "always()",
},
},
},
@ -138,14 +142,15 @@ 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",
Uses: "org/repo/path@ref",
},
RunContext: &RunContext{
Config: &Config{},
@ -172,6 +177,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: "org/repo/path@ref",
},
RunContext: &RunContext{
ActionRepository: "org/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 +229,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

View File

@ -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
}
}
}

View File

@ -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"])
}

View File

@ -12,7 +12,7 @@ import (
// ExpressionEvaluator is the interface for evaluating expressions
type ExpressionEvaluator interface {
evaluate(string, bool) (interface{}, error)
evaluate(string, exprparser.DefaultStatusCheck) (interface{}, error)
EvaluateYamlNode(node *yaml.Node) error
Interpolate(string) string
}
@ -115,9 +115,9 @@ type expressionEvaluator struct {
interpreter exprparser.Interpreter
}
func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) {
func (ee expressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
log.Debugf("evaluating expression '%s'", in)
evaluated, err := ee.interpreter.Evaluate(in, isIfExpression)
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
log.Debugf("expression '%s' evaluated to '%t'", in, evaluated)
return evaluated, err
}
@ -131,7 +131,7 @@ func (ee expressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
return nil
}
expr, _ := rewriteSubExpression(in, false)
res, err := ee.evaluate(expr, false)
res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
if err != nil {
return err
}
@ -201,7 +201,7 @@ func (ee expressionEvaluator) Interpolate(in string) string {
}
expr, _ := rewriteSubExpression(in, true)
evaluated, err := ee.evaluate(expr, false)
evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
if err != nil {
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
return ""
@ -216,10 +216,10 @@ func (ee expressionEvaluator) Interpolate(in string) string {
}
// EvalBool evaluates an expression against given evaluator
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
func EvalBool(evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
nextExpr, _ := rewriteSubExpression(expr, false)
evaluated, err := evaluator.evaluate(nextExpr, true)
evaluated, err := evaluator.evaluate(nextExpr, defaultStatusCheck)
if err != nil {
return false, err
}

View File

@ -7,6 +7,7 @@ import (
"sort"
"testing"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
assert "github.com/stretchr/testify/assert"
yaml "gopkg.in/yaml.v3"
@ -135,7 +136,7 @@ func TestEvaluateRunContext(t *testing.T) {
table := table
t.Run(table.in, func(t *testing.T) {
assertObject := assert.New(t)
out, err := ee.evaluate(table.in, false)
out, err := ee.evaluate(table.in, exprparser.DefaultStatusCheckNone)
if table.errMesg == "" {
assertObject.NoError(err, table.in)
assertObject.Equal(table.out, out, table.in)
@ -175,7 +176,7 @@ func TestEvaluateStep(t *testing.T) {
table := table
t.Run(table.in, func(t *testing.T) {
assertObject := assert.New(t)
out, err := ee.evaluate(table.in, false)
out, err := ee.evaluate(table.in, exprparser.DefaultStatusCheckNone)
if table.errMesg == "" {
assertObject.NoError(err, table.in)
assertObject.Equal(table.out, out, table.in)

View File

@ -21,7 +21,7 @@ type jobInfo interface {
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)
var postExecutor common.Executor
steps = append(steps, func(ctx context.Context) error {
if len(info.matrix()) > 0 {
@ -72,10 +72,15 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
})(withStepLogger(ctx, stepName))
})
postSteps = append([]common.Executor{step.post()}, postSteps...)
// run the post exector in reverse order
if postExecutor != nil {
postExecutor = step.post().Finally(postExecutor)
} else {
postExecutor = step.post()
}
}
postSteps = append(postSteps, func(ctx context.Context) error {
postExecutor = postExecutor.Finally(func(ctx context.Context) error {
jobError := common.JobError(ctx)
if jobError != nil {
info.result("failure")
@ -93,7 +98,9 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
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())
return common.NewPipelineExecutor(pipeline...).
Finally(postExecutor).
Finally(info.interpolateOutputs()).
Finally(info.closeContainer())
}

View File

@ -20,6 +20,7 @@ import (
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
)
@ -346,7 +347,7 @@ func (rc *RunContext) hostname() string {
func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
job := rc.Run.Job()
l := common.Logger(ctx)
runJob, err := EvalBool(rc.ExprEval, job.If.Value)
runJob, err := EvalBool(rc.ExprEval, job.If.Value, exprparser.DefaultStatusCheckSuccess)
if err != nil {
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, err)
}

View File

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
@ -155,7 +156,7 @@ func TestRunContext_EvalBool(t *testing.T) {
table := table
t.Run(table.in, func(t *testing.T) {
assertObject := assert.New(t)
b, err := EvalBool(rc.ExprEval, table.in)
b, err := EvalBool(rc.ExprEval, table.in, exprparser.DefaultStatusCheckSuccess)
if table.wantErr {
assertObject.Error(err)
}

View File

@ -173,6 +173,7 @@ func TestRunEvent(t *testing.T) {
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
{workdir, "actions-environment-and-context-tests", "push", "", platforms},
{workdir, "uses-action-with-pre-and-post-step", "push", "", platforms},
{"../model/testdata", "strategy", "push", "", platforms}, // TODO: move all testdata into pkg so we can validate it with planner and runner
// {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes
{"../model/testdata", "container-volumes", "push", "", platforms},

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
)
@ -18,14 +19,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 "Main"
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 +73,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
return err
}
runStep, err := isStepEnabled(ctx, step)
runStep, err := isStepEnabled(ctx, ifExpression, step, stage)
if err != nil {
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
@ -45,7 +81,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, ifExpression)
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusSkipped
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
return nil
@ -55,14 +91,14 @@ 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 Run %s %s", stage, stepString)
err = executor(ctx)
if err == nil {
common.Logger(ctx).Infof(" \u2705 Success - %s", stepString)
common.Logger(ctx).Infof(" \u2705 Success - %s %s", stage, stepString)
} else {
common.Logger(ctx).Errorf(" \u274C Failure - %s", stepString)
common.Logger(ctx).Errorf(" \u274C Failure - %s %s", stage, stepString)
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
if stepModel.ContinueOnError {
@ -129,12 +165,19 @@ 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, stage stepStage) (bool, error) {
rc := step.getRunContext()
runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), step.getStepModel().If.Value)
var defaultStatusCheck exprparser.DefaultStatusCheck
if stage == stepStagePost {
defaultStatusCheck = exprparser.DefaultStatusCheckAlways
} else {
defaultStatusCheck = exprparser.DefaultStatusCheckSuccess
}
runStep, err := EvalBool(rc.NewStepExpressionEvaluator(step), expr, defaultStatusCheck)
if err != nil {
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", step.getStepModel().If.Value, err)
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err)
}
return runStep, nil

View File

@ -11,28 +11,23 @@ import (
"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
Step *model.Step
RunContext *RunContext
compositeRunContext *RunContext
compositeSteps *compositeSteps
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 {
return func(ctx context.Context) error {
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
localReader := func(ctx context.Context) actionYamlReader {
@ -56,16 +51,27 @@ func (sal *stepActionLocal) main() common.Executor {
}
sal.action = actionModel
log.Debugf("Read action %v from '%s'", sal.action, "Unknown")
// run local pre step only for composite actions, to allow to run
// inside pre steps
if sal.action.Runs.Using == model.ActionRunsUsingComposite {
sal.RunContext.setupActionInputs(sal)
return runStepExecutor(sal, stepStagePre, runPreStep(sal)).If(hasPreStep(sal)).If(shouldRunPreStep(sal))(ctx)
}
return nil
}
}
func (sal *stepActionLocal) main() common.Executor {
return runStepExecutor(sal, stepStageMain, func(ctx context.Context) error {
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
return sal.runAction(sal, actionDir, nil)(ctx)
})
}
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 +86,31 @@ 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
}
func (sal *stepActionLocal) getCompositeRunContext() *RunContext {
if sal.compositeRunContext == nil {
actionDir := filepath.Join(sal.RunContext.Config.Workdir, sal.Step.Uses)
_, containerActionDir := getContainerActionPaths(sal.getStepModel(), actionDir, sal.RunContext)
sal.compositeRunContext = newCompositeRunContext(sal.RunContext, sal, containerActionDir)
sal.compositeSteps = sal.compositeRunContext.compositeExecutor(sal.action)
}
return sal.compositeRunContext
}
func (sal *stepActionLocal) getCompositeSteps() *compositeSteps {
return sal.compositeSteps
}

View File

@ -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 {
@ -80,22 +82,252 @@ func TestStepActionLocalTest(t *testing.T) {
return nil
})
err := sal.main()(ctx)
err := sal.pre()(ctx)
assert.Nil(t, err)
err = sal.main()(ctx)
assert.Nil(t, err)
cm.AssertExpectations(t)
salm.AssertExpectations(t)
}
func TestStepActionLocalPrePost(t *testing.T) {
func TestStepActionLocalPre(t *testing.T) {
cm := &containerMock{}
salm := &stepActionLocalMocks{}
ctx := context.Background()
sal := &stepActionLocal{}
sal := &stepActionLocal{
readAction: salm.readAction,
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)
err := sal.pre()(ctx)
assert.Nil(t, err)
err = sal.post()(ctx)
assert.Nil(t, err)
cm.AssertExpectations(t)
salm.AssertExpectations(t)
}
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)
})
}
}

View File

@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@ -13,88 +14,102 @@ import (
"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
}
Step *model.Step
RunContext *RunContext
compositeRunContext *RunContext
compositeSteps *compositeSteps
readAction readAction
runAction runAction
action *model.Action
env map[string]string
remoteAction *remoteAction
}
var (
stepActionRemoteNewCloneExecutor = common.NewGitCloneExecutor
)
func (sar *stepActionRemote) main() common.Executor {
func (sar *stepActionRemote) pre() 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)
}
return common.NewPipelineExecutor(
func(ctx context.Context) error {
sar.remoteAction = newRemoteAction(sar.Step.Uses)
if sar.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
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
github := sar.RunContext.getGithubContext()
if sar.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: sar.remoteAction.CloneURL(),
Ref: sar.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, sar.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, sar.remoteAction.Path, filename))
return f, f, err
}
}
return common.NewPipelineExecutor(
ntErr,
func(ctx context.Context) error {
actionModel, err := sar.readAction(sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), ioutil.WriteFile)
sar.action = actionModel
return err
},
)(ctx)
},
func(ctx context.Context) error {
sar.RunContext.setupActionInputs(sar)
return nil
},
runStepExecutor(sar, stepStagePre, runPreStep(sar)).If(hasPreStep(sar)).If(shouldRunPreStep(sar)))
}
func (sar *stepActionRemote) main() common.Executor {
return runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
github := sar.RunContext.getGithubContext()
if remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
if sar.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),
sar.runAction(sar, actionDir, sar.remoteAction),
)(ctx)
})
}
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,10 +124,43 @@ func (sar *stepActionRemote) getEnv() *map[string]string {
return &sar.env
}
func (sar *stepActionRemote) getIfExpression(stage stepStage) string {
switch stage {
case stepStagePre:
github := sar.RunContext.getGithubContext()
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
// skip local checkout pre step
return "false"
}
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
}
func (sar *stepActionRemote) getCompositeRunContext() *RunContext {
if sar.compositeRunContext == nil {
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
actionLocation := path.Join(actionDir, sar.remoteAction.Path)
_, containerActionDir := getContainerActionPaths(sar.getStepModel(), actionLocation, sar.RunContext)
sar.compositeRunContext = newCompositeRunContext(sar.RunContext, sar, containerActionDir)
sar.compositeSteps = sar.compositeRunContext.compositeExecutor(sar.action)
}
return sar.compositeRunContext
}
func (sar *stepActionRemote) getCompositeSteps() *compositeSteps {
return sar.compositeSteps
}
type remoteAction struct {
URL string
Org string

View File

@ -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,430 @@ func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string,
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
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
}
}
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
sar := &stepActionRemote{
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "github.com",
runError error
}{
{
name: "run-successful",
stepModel: &model.Step{
ID: "step",
Uses: "remote/action@v1",
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
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: true,
read: true,
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"),
},
}
for _, tt := range table {
t.Run(tt.name, func(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: "github.com",
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
StepResults: map[string]*model.StepResult{},
JobContainer: cm,
},
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)
})
}
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 })
}
err := sar.pre()(ctx)
if err == nil {
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"])
sarm.AssertExpectations(t)
cm.AssertExpectations(t)
})
}
}
func TestStepActionRemotePre(t *testing.T) {
table := []struct {
name string
stepModel *model.Step
}{
{
name: "run-pre",
stepModel: &model.Step{
Uses: "org/repo/path@ref",
},
},
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
clonedAction := false
sarm := &stepActionRemoteMocks{}
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{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://github.com",
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
},
readAction: sarm.readAction,
}
suffixMatcher := func(suffix string) interface{} {
return mock.MatchedBy(func(actionDir string) bool {
return strings.HasSuffix(actionDir, suffix)
})
}
sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
err := sar.pre()(ctx)
assert.Nil(t, err)
assert.Equal(t, true, clonedAction)
sarm.AssertExpectations(t)
})
}
}
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",
},
},
},
StepResults: map[string]*model.StepResult{},
JobContainer: cm,
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,
},
},
Step: &model.Step{
Uses: "remote/action@v1",
{
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,
},
},
readAction: sarm.readAction,
runAction: sarm.runAction,
}
suffixMatcher := func(suffix string) interface{} {
return mock.MatchedBy(func(actionDir string) bool {
return strings.HasSuffix(actionDir, suffix)
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)
})
}
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"), newRemoteAction(sar.Step.Uses)).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)
}

View File

@ -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

View File

@ -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)

View File

@ -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.getIfExpression(stepStageMain), step, stepStageMain))
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, stepStageMain))
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, stepStageMain))
// failure()
step = createTestStep(t, "if: failure()")
assertObject.False(isStepEnabled(context.Background(), step))
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
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, stepStageMain))
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, stepStageMain))
// always()
step = createTestStep(t, "if: always()")
assertObject.True(isStepEnabled(context.Background(), step))
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
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, stepStageMain))
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, stepStageMain))
}

View File

@ -0,0 +1,7 @@
name: "last action check"
description: "last action check"
runs:
using: "node16"
main: main.js
post: post.js

View File

@ -0,0 +1,17 @@
const pre = process.env['ACTION_OUTPUT_PRE'];
const main = process.env['ACTION_OUTPUT_MAIN'];
const post = process.env['ACTION_OUTPUT_POST'];
console.log({pre, main, post});
if (pre !== 'pre') {
throw new Error(`Expected 'pre' but got '${pre}'`);
}
if (main !== 'main') {
throw new Error(`Expected 'main' but got '${main}'`);
}
if (post !== 'post') {
throw new Error(`Expected 'post' but got '${post}'`);
}

View File

@ -0,0 +1,15 @@
name: uses-action-with-pre-and-post-step
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./uses-action-with-pre-and-post-step/last-action
- uses: nektos/act-test-actions/js-with-pre-and-post-step@main
with:
pre: true
post: true
- run: |
cat $GITHUB_ENV

View File

@ -0,0 +1,13 @@
name: "Action with pre and post"
description: "Action with pre and post"
inputs:
step:
description: "step"
required: true
runs:
using: "node16"
pre: pre.js
main: main.js
post: post.js

View File

@ -0,0 +1,3 @@
const { appendFileSync } = require('fs');
const step = process.env['INPUT_STEP'];
appendFileSync(process.env['GITHUB_ENV'], `;${step}`, { encoding:'utf-8' })

View File

@ -0,0 +1,3 @@
const { appendFileSync } = require('fs');
const step = process.env['INPUT_STEP'];
appendFileSync(process.env['GITHUB_ENV'], `;${step}-post`, { encoding:'utf-8' })

View File

@ -0,0 +1 @@
console.log('pre');

View File

@ -0,0 +1,12 @@
name: "Test Composite Action"
description: "Test action uses composite"
runs:
using: "composite"
steps:
- uses: ./uses-composite-with-pre-and-post-steps/action-with-pre-and-post
with:
step: step1
- uses: ./uses-composite-with-pre-and-post-steps/action-with-pre-and-post
with:
step: step2

View File

@ -0,0 +1,7 @@
name: "last action check"
description: "last action check"
runs:
using: "node16"
main: main.js
post: post.js

View File

@ -0,0 +1,7 @@
const output = process.env['STEP_OUTPUT_TEST'];
const expected = 'empty;step1;step2;step2-post;step1-post';
console.log(output);
if (output !== expected) {
throw new Error(`Expected '${expected}' but got '${output}'`);
}

View File

@ -0,0 +1,11 @@
name: uses-composite-with-pre-and-post-steps
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: ./uses-composite-with-pre-and-post-steps/last-action
- uses: actions/checkout@v2
- run: echo -n "STEP_OUTPUT_TEST=empty" >> $GITHUB_ENV
- uses: ./uses-composite-with-pre-and-post-steps/composite_action