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:
parent
ebb408f373
commit
943a0e6eea
37 changed files with 1560 additions and 350 deletions
|
@ -37,7 +37,7 @@ func TestFunctionContains(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -66,7 +66,7 @@ func TestFunctionStartsWith(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -95,7 +95,7 @@ func TestFunctionEndsWith(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -122,7 +122,7 @@ func TestFunctionJoin(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -148,7 +148,7 @@ func TestFunctionToJSON(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -171,7 +171,7 @@ func TestFunctionFromJSON(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -197,7 +197,7 @@ func TestFunctionHashFiles(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
workdir, err := filepath.Abs("testdata")
|
workdir, err := filepath.Abs("testdata")
|
||||||
assert.Nil(t, err)
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -234,7 +234,7 @@ func TestFunctionFormat(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
if tt.error != nil {
|
||||||
assert.Equal(t, tt.error, err.Error())
|
assert.Equal(t, tt.error, err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,8 +30,32 @@ type Config struct {
|
||||||
Context string
|
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 {
|
type Interpreter interface {
|
||||||
Evaluate(input string, isIfExpression bool) (interface{}, error)
|
Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type interperterImpl struct {
|
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, "${{")
|
input = strings.TrimPrefix(input, "${{")
|
||||||
if isIfExpression && input == "" {
|
if defaultStatusCheck != DefaultStatusCheckNone && input == "" {
|
||||||
input = "success()"
|
input = "success()"
|
||||||
}
|
}
|
||||||
parser := actionlint.NewExprParser()
|
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)
|
return nil, fmt.Errorf("Failed to parse: %s", err.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isIfExpression {
|
if defaultStatusCheck != DefaultStatusCheckNone {
|
||||||
hasStatusCheckFunction := false
|
hasStatusCheckFunction := false
|
||||||
actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
|
actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
|
||||||
if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
|
if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
|
||||||
|
@ -72,7 +96,7 @@ func (impl *interperterImpl) Evaluate(input string, isIfExpression bool) (interf
|
||||||
exprNode = &actionlint.LogicalOpNode{
|
exprNode = &actionlint.LogicalOpNode{
|
||||||
Kind: actionlint.LogicalOpNodeKindAnd,
|
Kind: actionlint.LogicalOpNodeKindAnd,
|
||||||
Left: &actionlint.FuncCallNode{
|
Left: &actionlint.FuncCallNode{
|
||||||
Callee: "success",
|
Callee: defaultStatusCheck.String(),
|
||||||
Args: []actionlint.ExprNode{},
|
Args: []actionlint.ExprNode{},
|
||||||
},
|
},
|
||||||
Right: exprNode,
|
Right: exprNode,
|
||||||
|
@ -361,7 +385,7 @@ func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value {
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to parse the string as a number
|
// 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 {
|
if err != nil {
|
||||||
return reflect.ValueOf(math.NaN())
|
return reflect.ValueOf(math.NaN())
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestLiterals(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -93,7 +93,7 @@ func TestOperators(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 != "" {
|
if tt.error != "" {
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
assert.Equal(t, tt.error, err.Error())
|
assert.Equal(t, tt.error, err.Error())
|
||||||
|
@ -146,7 +146,7 @@ func TestOperatorsCompare(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
@ -509,7 +509,7 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
||||||
|
@ -607,7 +607,7 @@ func TestContexts(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
|
|
@ -49,6 +49,10 @@ type ActionRuns struct {
|
||||||
Using ActionRunsUsing `yaml:"using"`
|
Using ActionRunsUsing `yaml:"using"`
|
||||||
Env map[string]string `yaml:"env"`
|
Env map[string]string `yaml:"env"`
|
||||||
Main string `yaml:"main"`
|
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"`
|
Image string `yaml:"image"`
|
||||||
Entrypoint string `yaml:"entrypoint"`
|
Entrypoint string `yaml:"entrypoint"`
|
||||||
Args []string `yaml:"args"`
|
Args []string `yaml:"args"`
|
||||||
|
@ -90,5 +94,13 @@ func ReadAction(in io.Reader) (*Action, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set defaults
|
||||||
|
if a.Runs.PreIf == "" {
|
||||||
|
a.Runs.PreIf = "always()"
|
||||||
|
}
|
||||||
|
if a.Runs.PostIf == "" {
|
||||||
|
a.Runs.PostIf = "always()"
|
||||||
|
}
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,4 +42,5 @@ type StepResult struct {
|
||||||
Outputs map[string]string `json:"outputs"`
|
Outputs map[string]string `json:"outputs"`
|
||||||
Conclusion stepStatus `json:"conclusion"`
|
Conclusion stepStatus `json:"conclusion"`
|
||||||
Outcome stepStatus `json:"outcome"`
|
Outcome stepStatus `json:"outcome"`
|
||||||
|
State map[string]string
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ type actionStep interface {
|
||||||
step
|
step
|
||||||
|
|
||||||
getActionModel() *model.Action
|
getActionModel() *model.Action
|
||||||
|
getCompositeRunContext() *RunContext
|
||||||
|
getCompositeSteps() *compositeSteps
|
||||||
}
|
}
|
||||||
|
|
||||||
type readAction func(step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
|
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
|
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 {
|
func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor {
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
stepModel := step.getStepModel()
|
||||||
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
actionPath := ""
|
actionPath := ""
|
||||||
if remoteAction != nil && remoteAction.Path != "" {
|
if remoteAction != nil && remoteAction.Path != "" {
|
||||||
actionPath = remoteAction.Path
|
actionPath = remoteAction.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
log.Debugf("About to run action %v", action)
|
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
|
// time, we don't have all environment prepared
|
||||||
mergeIntoMap(step.getEnv(), rc.withGithubEnv(map[string]string{}))
|
mergeIntoMap(step.getEnv(), rc.withGithubEnv(map[string]string{}))
|
||||||
|
|
||||||
|
populateEnvsFromSavedState(step.getEnv(), step, rc)
|
||||||
populateEnvsFromInput(step.getEnv(), action, rc)
|
populateEnvsFromInput(step.getEnv(), action, rc)
|
||||||
|
|
||||||
actionLocation := path.Join(actionDir, actionPath)
|
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)
|
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 {
|
switch action.Runs.Using {
|
||||||
case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16:
|
case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16:
|
||||||
if err := maybeCopyToActionDir(); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
|
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)
|
return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
|
||||||
case model.ActionRunsUsingComposite:
|
case model.ActionRunsUsingComposite:
|
||||||
if err := maybeCopyToActionDir(); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return execAsComposite(step, containerActionDir)(ctx)
|
|
||||||
|
return execAsComposite(step)(ctx)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
|
||||||
model.ActionRunsUsingDocker,
|
model.ActionRunsUsingDocker,
|
||||||
|
@ -359,134 +368,36 @@ func newStepContainer(ctx context.Context, step step, image string, cmd []string
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func execAsComposite(step actionStep, containerActionDir string) common.Executor {
|
func (rc *RunContext) setupActionInputs(step actionStep) {
|
||||||
rc := step.getRunContext()
|
if step.getActionModel() == nil {
|
||||||
|
// e.g. local checkout skip has no action model
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stepModel := step.getStepModel()
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
eval := rc.NewExpressionEvaluator()
|
eval := rc.NewExpressionEvaluator()
|
||||||
|
|
||||||
inputs := make(map[string]interface{})
|
inputs := make(map[string]interface{})
|
||||||
for k, input := range action.Inputs {
|
for k, input := range action.Inputs {
|
||||||
inputs[k] = eval.Interpolate(input.Default)
|
inputs[k] = eval.Interpolate(input.Default)
|
||||||
}
|
}
|
||||||
if step.getStepModel().With != nil {
|
if stepModel.With != nil {
|
||||||
for k, v := range step.getStepModel().With {
|
for k, v := range stepModel.With {
|
||||||
inputs[k] = eval.Interpolate(v)
|
inputs[k] = eval.Interpolate(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
env := make(map[string]string)
|
rc.Inputs = inputs
|
||||||
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
|
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
||||||
configCopy := *rc.Config
|
stepResult := rc.StepResults[step.getStepModel().ID]
|
||||||
configCopy.Secrets = nil
|
if stepResult != nil {
|
||||||
|
for name, value := range stepResult.State {
|
||||||
// create a run context for the composite action to run in
|
envName := fmt.Sprintf("STATE_%s", name)
|
||||||
compositerc := &RunContext{
|
(*env)[envName] = value
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
195
pkg/runner/action_composite.go
Normal file
195
pkg/runner/action_composite.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,8 @@ runs:
|
||||||
Runs: model.ActionRuns{
|
Runs: model.ActionRuns{
|
||||||
Using: "node16",
|
Using: "node16",
|
||||||
Main: "main.js",
|
Main: "main.js",
|
||||||
|
PreIf: "always()",
|
||||||
|
PostIf: "always()",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -59,6 +61,8 @@ runs:
|
||||||
Runs: model.ActionRuns{
|
Runs: model.ActionRuns{
|
||||||
Using: "node16",
|
Using: "node16",
|
||||||
Main: "main.js",
|
Main: "main.js",
|
||||||
|
PreIf: "always()",
|
||||||
|
PostIf: "always()",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -140,12 +144,13 @@ func TestActionRunner(t *testing.T) {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
step actionStep
|
step actionStep
|
||||||
|
expectedEnv map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Test",
|
name: "with-input",
|
||||||
step: &stepActionRemote{
|
step: &stepActionRemote{
|
||||||
Step: &model.Step{
|
Step: &model.Step{
|
||||||
Uses: "repo@ref",
|
Uses: "org/repo/path@ref",
|
||||||
},
|
},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{},
|
||||||
|
@ -172,6 +177,47 @@ func TestActionRunner(t *testing.T) {
|
||||||
},
|
},
|
||||||
env: map[string]string{},
|
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 })
|
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 {
|
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 })
|
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
|
tt.step.getRunContext().JobContainer = cm
|
||||||
|
|
|
@ -63,6 +63,9 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||||
case resumeCommand:
|
case resumeCommand:
|
||||||
resumeCommand = ""
|
resumeCommand = ""
|
||||||
logger.Infof(" \U00002699 %s", line)
|
logger.Infof(" \U00002699 %s", line)
|
||||||
|
case "save-state":
|
||||||
|
logger.Infof(" \U0001f4be %s", line)
|
||||||
|
rc.saveState(ctx, kvPairs, arg)
|
||||||
default:
|
default:
|
||||||
logger.Infof(" \U00002753 %s", line)
|
logger.Infof(" \U00002753 %s", line)
|
||||||
}
|
}
|
||||||
|
@ -141,3 +144,15 @@ func unescapeKvPairs(kvPairs map[string]string) map[string]string {
|
||||||
}
|
}
|
||||||
return kvPairs
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -173,3 +173,21 @@ func TestAddmaskUsemask(t *testing.T) {
|
||||||
|
|
||||||
a.Equal("[testjob] \U00002699 ***\n[testjob] \U00002699 ::set-output:: = token=***\n", re)
|
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"])
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
// ExpressionEvaluator is the interface for evaluating expressions
|
// ExpressionEvaluator is the interface for evaluating expressions
|
||||||
type ExpressionEvaluator interface {
|
type ExpressionEvaluator interface {
|
||||||
evaluate(string, bool) (interface{}, error)
|
evaluate(string, exprparser.DefaultStatusCheck) (interface{}, error)
|
||||||
EvaluateYamlNode(node *yaml.Node) error
|
EvaluateYamlNode(node *yaml.Node) error
|
||||||
Interpolate(string) string
|
Interpolate(string) string
|
||||||
}
|
}
|
||||||
|
@ -115,9 +115,9 @@ type expressionEvaluator struct {
|
||||||
interpreter exprparser.Interpreter
|
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)
|
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)
|
log.Debugf("expression '%s' evaluated to '%t'", in, evaluated)
|
||||||
return evaluated, err
|
return evaluated, err
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ func (ee expressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
expr, _ := rewriteSubExpression(in, false)
|
expr, _ := rewriteSubExpression(in, false)
|
||||||
res, err := ee.evaluate(expr, false)
|
res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ func (ee expressionEvaluator) Interpolate(in string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
expr, _ := rewriteSubExpression(in, true)
|
expr, _ := rewriteSubExpression(in, true)
|
||||||
evaluated, err := ee.evaluate(expr, false)
|
evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
||||||
return ""
|
return ""
|
||||||
|
@ -216,10 +216,10 @@ func (ee expressionEvaluator) Interpolate(in string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvalBool evaluates an expression against given evaluator
|
// 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)
|
nextExpr, _ := rewriteSubExpression(expr, false)
|
||||||
|
|
||||||
evaluated, err := evaluator.evaluate(nextExpr, true)
|
evaluated, err := evaluator.evaluate(nextExpr, defaultStatusCheck)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
@ -135,7 +136,7 @@ func TestEvaluateRunContext(t *testing.T) {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.in, func(t *testing.T) {
|
t.Run(table.in, func(t *testing.T) {
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
out, err := ee.evaluate(table.in, false)
|
out, err := ee.evaluate(table.in, exprparser.DefaultStatusCheckNone)
|
||||||
if table.errMesg == "" {
|
if table.errMesg == "" {
|
||||||
assertObject.NoError(err, table.in)
|
assertObject.NoError(err, table.in)
|
||||||
assertObject.Equal(table.out, out, table.in)
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
@ -175,7 +176,7 @@ func TestEvaluateStep(t *testing.T) {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.in, func(t *testing.T) {
|
t.Run(table.in, func(t *testing.T) {
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
out, err := ee.evaluate(table.in, false)
|
out, err := ee.evaluate(table.in, exprparser.DefaultStatusCheckNone)
|
||||||
if table.errMesg == "" {
|
if table.errMesg == "" {
|
||||||
assertObject.NoError(err, table.in)
|
assertObject.NoError(err, table.in)
|
||||||
assertObject.Equal(table.out, out, table.in)
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
|
|
@ -21,7 +21,7 @@ type jobInfo interface {
|
||||||
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
||||||
steps := make([]common.Executor, 0)
|
steps := make([]common.Executor, 0)
|
||||||
preSteps := 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 {
|
steps = append(steps, func(ctx context.Context) error {
|
||||||
if len(info.matrix()) > 0 {
|
if len(info.matrix()) > 0 {
|
||||||
|
@ -72,10 +72,15 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||||
})(withStepLogger(ctx, stepName))
|
})(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)
|
jobError := common.JobError(ctx)
|
||||||
if jobError != nil {
|
if jobError != nil {
|
||||||
info.result("failure")
|
info.result("failure")
|
||||||
|
@ -93,7 +98,9 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||||
pipeline := make([]common.Executor, 0)
|
pipeline := make([]common.Executor, 0)
|
||||||
pipeline = append(pipeline, preSteps...)
|
pipeline = append(pipeline, preSteps...)
|
||||||
pipeline = append(pipeline, steps...)
|
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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/container"
|
"github.com/nektos/act/pkg/container"
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -346,7 +347,7 @@ func (rc *RunContext) hostname() string {
|
||||||
func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||||
job := rc.Run.Job()
|
job := rc.Run.Job()
|
||||||
l := common.Logger(ctx)
|
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 {
|
if err != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, err)
|
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -155,7 +156,7 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.in, func(t *testing.T) {
|
t.Run(table.in, func(t *testing.T) {
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
b, err := EvalBool(rc.ExprEval, table.in)
|
b, err := EvalBool(rc.ExprEval, table.in, exprparser.DefaultStatusCheckSuccess)
|
||||||
if table.wantErr {
|
if table.wantErr {
|
||||||
assertObject.Error(err)
|
assertObject.Error(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,6 +173,7 @@ func TestRunEvent(t *testing.T) {
|
||||||
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
{workdir, "job-status-check", "push", "job 'fail' failed", platforms},
|
||||||
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
{workdir, "if-expressions", "push", "Job 'mytest' failed", platforms},
|
||||||
{workdir, "actions-environment-and-context-tests", "push", "", 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
|
{"../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
|
// {"testdata", "issue-228", "push", "", platforms, }, // TODO [igni]: Remove this once everything passes
|
||||||
{"../model/testdata", "container-volumes", "push", "", platforms},
|
{"../model/testdata", "container-volumes", "push", "", platforms},
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -18,14 +19,49 @@ type step interface {
|
||||||
getRunContext() *RunContext
|
getRunContext() *RunContext
|
||||||
getStepModel() *model.Step
|
getStepModel() *model.Step
|
||||||
getEnv() *map[string]string
|
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 {
|
return func(ctx context.Context) error {
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
stepModel := step.getStepModel()
|
||||||
|
|
||||||
rc.CurrentStep = stepModel.ID
|
ifExpression := step.getIfExpression(stage)
|
||||||
|
rc.CurrentStep = stage.getStepName(stepModel)
|
||||||
|
|
||||||
rc.StepResults[rc.CurrentStep] = &model.StepResult{
|
rc.StepResults[rc.CurrentStep] = &model.StepResult{
|
||||||
Outcome: model.StepStatusSuccess,
|
Outcome: model.StepStatusSuccess,
|
||||||
Conclusion: model.StepStatusSuccess,
|
Conclusion: model.StepStatusSuccess,
|
||||||
|
@ -37,7 +73,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
runStep, err := isStepEnabled(ctx, step)
|
runStep, err := isStepEnabled(ctx, ifExpression, step, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].Conclusion = model.StepStatusFailure
|
||||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||||
|
@ -45,7 +81,7 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !runStep {
|
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].Conclusion = model.StepStatusSkipped
|
||||||
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusSkipped
|
||||||
return nil
|
return nil
|
||||||
|
@ -55,14 +91,14 @@ func runStepExecutor(step step, executor common.Executor) common.Executor {
|
||||||
if strings.Contains(stepString, "::add-mask::") {
|
if strings.Contains(stepString, "::add-mask::") {
|
||||||
stepString = "add-mask command"
|
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)
|
err = executor(ctx)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
common.Logger(ctx).Infof(" \u2705 Success - %s", stepString)
|
common.Logger(ctx).Infof(" \u2705 Success - %s %s", stage, stepString)
|
||||||
} else {
|
} 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
|
rc.StepResults[rc.CurrentStep].Outcome = model.StepStatusFailure
|
||||||
if stepModel.ContinueOnError {
|
if stepModel.ContinueOnError {
|
||||||
|
@ -129,12 +165,19 @@ func mergeEnv(step step) {
|
||||||
mergeIntoMap(env, rc.withGithubEnv(*env))
|
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()
|
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 {
|
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
|
return runStep, nil
|
||||||
|
|
|
@ -11,12 +11,13 @@ import (
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionLocal struct {
|
type stepActionLocal struct {
|
||||||
Step *model.Step
|
Step *model.Step
|
||||||
RunContext *RunContext
|
RunContext *RunContext
|
||||||
|
compositeRunContext *RunContext
|
||||||
|
compositeSteps *compositeSteps
|
||||||
runAction runAction
|
runAction runAction
|
||||||
readAction readAction
|
readAction readAction
|
||||||
env map[string]string
|
env map[string]string
|
||||||
|
@ -24,15 +25,9 @@ type stepActionLocal struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sal *stepActionLocal) pre() common.Executor {
|
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{}
|
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)
|
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||||
|
|
||||||
localReader := func(ctx context.Context) actionYamlReader {
|
localReader := func(ctx context.Context) actionYamlReader {
|
||||||
|
@ -56,16 +51,27 @@ func (sal *stepActionLocal) main() common.Executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
sal.action = actionModel
|
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)
|
return sal.runAction(sal, actionDir, nil)(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sal *stepActionLocal) post() common.Executor {
|
func (sal *stepActionLocal) post() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return runStepExecutor(sal, stepStagePost, runPostStep(sal)).If(hasPostStep(sal)).If(shouldRunPostStep(sal))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sal *stepActionLocal) getRunContext() *RunContext {
|
func (sal *stepActionLocal) getRunContext() *RunContext {
|
||||||
|
@ -80,6 +86,31 @@ func (sal *stepActionLocal) getEnv() *map[string]string {
|
||||||
return &sal.env
|
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 {
|
func (sal *stepActionLocal) getActionModel() *model.Action {
|
||||||
return sal.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
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@ package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionLocalMocks struct {
|
type stepActionLocalMocks struct {
|
||||||
|
@ -80,22 +82,252 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
err := sal.main()(ctx)
|
err := sal.pre()(ctx)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = sal.main()(ctx)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
salm.AssertExpectations(t)
|
salm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepActionLocalPrePost(t *testing.T) {
|
func TestStepActionLocalPre(t *testing.T) {
|
||||||
|
cm := &containerMock{}
|
||||||
|
salm := &stepActionLocalMocks{}
|
||||||
|
|
||||||
ctx := context.Background()
|
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)
|
err := sal.pre()(ctx)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err = sal.post()(ctx)
|
cm.AssertExpectations(t)
|
||||||
assert.Nil(t, err)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -13,49 +14,46 @@ import (
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionRemote struct {
|
type stepActionRemote struct {
|
||||||
Step *model.Step
|
Step *model.Step
|
||||||
RunContext *RunContext
|
RunContext *RunContext
|
||||||
|
compositeRunContext *RunContext
|
||||||
|
compositeSteps *compositeSteps
|
||||||
readAction readAction
|
readAction readAction
|
||||||
runAction runAction
|
runAction runAction
|
||||||
action *model.Action
|
action *model.Action
|
||||||
env map[string]string
|
env map[string]string
|
||||||
}
|
remoteAction *remoteAction
|
||||||
|
|
||||||
func (sar *stepActionRemote) pre() common.Executor {
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stepActionRemoteNewCloneExecutor = common.NewGitCloneExecutor
|
stepActionRemoteNewCloneExecutor = common.NewGitCloneExecutor
|
||||||
)
|
)
|
||||||
|
|
||||||
func (sar *stepActionRemote) main() common.Executor {
|
func (sar *stepActionRemote) pre() common.Executor {
|
||||||
sar.env = map[string]string{}
|
sar.env = map[string]string{}
|
||||||
|
|
||||||
return runStepExecutor(sar, func(ctx context.Context) error {
|
return common.NewPipelineExecutor(
|
||||||
remoteAction := newRemoteAction(sar.Step.Uses)
|
func(ctx context.Context) error {
|
||||||
if remoteAction == nil {
|
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)
|
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()
|
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")
|
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
|
||||||
gitClone := stepActionRemoteNewCloneExecutor(common.NewGitCloneExecutorInput{
|
gitClone := stepActionRemoteNewCloneExecutor(common.NewGitCloneExecutorInput{
|
||||||
URL: remoteAction.CloneURL(),
|
URL: sar.remoteAction.CloneURL(),
|
||||||
Ref: remoteAction.Ref,
|
Ref: sar.remoteAction.Ref,
|
||||||
Dir: actionDir,
|
Dir: actionDir,
|
||||||
Token: github.Token,
|
Token: github.Token,
|
||||||
})
|
})
|
||||||
|
@ -63,7 +61,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||||
if err := gitClone(ctx); err != nil {
|
if err := gitClone(ctx); err != nil {
|
||||||
if err.Error() == "short SHA references are not supported" {
|
if err.Error() == "short SHA references are not supported" {
|
||||||
err = errors.Cause(err)
|
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())
|
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" {
|
} else if err.Error() != "some refs were not updated" {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,7 +71,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||||
|
|
||||||
remoteReader := func(ctx context.Context) actionYamlReader {
|
remoteReader := func(ctx context.Context) actionYamlReader {
|
||||||
return func(filename string) (io.Reader, io.Closer, error) {
|
return func(filename string) (io.Reader, io.Closer, error) {
|
||||||
f, err := os.Open(filepath.Join(actionDir, remoteAction.Path, filename))
|
f, err := os.Open(filepath.Join(actionDir, sar.remoteAction.Path, filename))
|
||||||
return f, f, err
|
return f, f, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,20 +79,37 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
ntErr,
|
ntErr,
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
actionModel, err := sar.readAction(sar.Step, actionDir, remoteAction.Path, remoteReader(ctx), ioutil.WriteFile)
|
actionModel, err := sar.readAction(sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), ioutil.WriteFile)
|
||||||
sar.action = actionModel
|
sar.action = actionModel
|
||||||
log.Debugf("Read action %v from '%s'", sar.action, "Unknown")
|
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
sar.runAction(sar, actionDir, remoteAction),
|
)(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 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, "/", "-"))
|
||||||
|
|
||||||
|
return common.NewPipelineExecutor(
|
||||||
|
sar.runAction(sar, actionDir, sar.remoteAction),
|
||||||
)(ctx)
|
)(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sar *stepActionRemote) post() common.Executor {
|
func (sar *stepActionRemote) post() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return runStepExecutor(sar, stepStagePost, runPostStep(sar)).If(hasPostStep(sar)).If(shouldRunPostStep(sar))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sar *stepActionRemote) getRunContext() *RunContext {
|
func (sar *stepActionRemote) getRunContext() *RunContext {
|
||||||
|
@ -109,10 +124,43 @@ func (sar *stepActionRemote) getEnv() *map[string]string {
|
||||||
return &sar.env
|
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 {
|
func (sar *stepActionRemote) getActionModel() *model.Action {
|
||||||
return sar.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 {
|
type remoteAction struct {
|
||||||
URL string
|
URL string
|
||||||
Org string
|
Org string
|
||||||
|
|
|
@ -2,6 +2,7 @@ package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionRemoteMocks struct {
|
type stepActionRemoteMocks struct {
|
||||||
|
@ -25,11 +27,97 @@ func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string,
|
||||||
return args.Get(0).(func(context.Context) error)
|
return args.Get(0).(func(context.Context) error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepActionRemoteTest(t *testing.T) {
|
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: 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()
|
ctx := context.Background()
|
||||||
|
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
|
|
||||||
sarm := &stepActionRemoteMocks{}
|
sarm := &stepActionRemoteMocks{}
|
||||||
|
|
||||||
clonedAction := false
|
clonedAction := false
|
||||||
|
@ -61,9 +149,7 @@ func TestStepActionRemoteTest(t *testing.T) {
|
||||||
StepResults: map[string]*model.StepResult{},
|
StepResults: map[string]*model.StepResult{},
|
||||||
JobContainer: cm,
|
JobContainer: cm,
|
||||||
},
|
},
|
||||||
Step: &model.Step{
|
Step: tt.stepModel,
|
||||||
Uses: "remote/action@v1",
|
|
||||||
},
|
|
||||||
readAction: sarm.readAction,
|
readAction: sarm.readAction,
|
||||||
runAction: sarm.runAction,
|
runAction: sarm.runAction,
|
||||||
}
|
}
|
||||||
|
@ -74,29 +160,297 @@ func TestStepActionRemoteTest(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tt.mocks.env {
|
||||||
cm.On("UpdateFromImageEnv", &sar.env).Return(func(ctx context.Context) error { return nil })
|
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("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 })
|
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)
|
|
||||||
}
|
}
|
||||||
|
if tt.mocks.read {
|
||||||
func TestStepActionRemotePrePost(t *testing.T) {
|
sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||||
ctx := context.Background()
|
}
|
||||||
|
if tt.mocks.run {
|
||||||
sar := &stepActionRemote{}
|
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)
|
err := sar.pre()(ctx)
|
||||||
assert.Nil(t, err)
|
if err == nil {
|
||||||
|
err = sar.main()(ctx)
|
||||||
err = sar.post()(ctx)
|
}
|
||||||
assert.Nil(t, err)
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (sd *stepDocker) pre() common.Executor {
|
||||||
func (sd *stepDocker) main() common.Executor {
|
func (sd *stepDocker) main() common.Executor {
|
||||||
sd.env = map[string]string{}
|
sd.env = map[string]string{}
|
||||||
|
|
||||||
return runStepExecutor(sd, sd.runUsesContainer())
|
return runStepExecutor(sd, stepStageMain, sd.runUsesContainer())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *stepDocker) post() common.Executor {
|
func (sd *stepDocker) post() common.Executor {
|
||||||
|
@ -47,6 +47,10 @@ func (sd *stepDocker) getEnv() *map[string]string {
|
||||||
return &sd.env
|
return &sd.env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sd *stepDocker) getIfExpression(stage stepStage) string {
|
||||||
|
return sd.Step.If.Value
|
||||||
|
}
|
||||||
|
|
||||||
func (sd *stepDocker) runUsesContainer() common.Executor {
|
func (sd *stepDocker) runUsesContainer() common.Executor {
|
||||||
rc := sd.RunContext
|
rc := sd.RunContext
|
||||||
step := sd.Step
|
step := sd.Step
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (sr *stepRun) pre() common.Executor {
|
||||||
func (sr *stepRun) main() common.Executor {
|
func (sr *stepRun) main() common.Executor {
|
||||||
sr.env = map[string]string{}
|
sr.env = map[string]string{}
|
||||||
|
|
||||||
return runStepExecutor(sr, common.NewPipelineExecutor(
|
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
||||||
sr.setupShellCommandExecutor(),
|
sr.setupShellCommandExecutor(),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.Step.WorkingDirectory)(ctx)
|
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
|
return &sr.env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sr *stepRun) getIfExpression(stage stepStage) string {
|
||||||
|
return sr.Step.If.Value
|
||||||
|
}
|
||||||
|
|
||||||
func (sr *stepRun) setupShellCommandExecutor() common.Executor {
|
func (sr *stepRun) setupShellCommandExecutor() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
scriptName, script, err := sr.setupShellCommand(ctx)
|
scriptName, script, err := sr.setupShellCommand(ctx)
|
||||||
|
|
|
@ -230,49 +230,49 @@ func TestIsStepEnabled(t *testing.T) {
|
||||||
|
|
||||||
// success()
|
// success()
|
||||||
step := createTestStep(t, "if: 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 = createTestStep(t, "if: success()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusSuccess,
|
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 = createTestStep(t, "if: success()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusFailure,
|
Conclusion: model.StepStatusFailure,
|
||||||
}
|
}
|
||||||
assertObject.False(isStepEnabled(context.Background(), step))
|
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||||
|
|
||||||
// failure()
|
// failure()
|
||||||
step = createTestStep(t, "if: 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 = createTestStep(t, "if: failure()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusSuccess,
|
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 = createTestStep(t, "if: failure()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusFailure,
|
Conclusion: model.StepStatusFailure,
|
||||||
}
|
}
|
||||||
assertObject.True(isStepEnabled(context.Background(), step))
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||||
|
|
||||||
// always()
|
// always()
|
||||||
step = createTestStep(t, "if: 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 = createTestStep(t, "if: always()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusSuccess,
|
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 = createTestStep(t, "if: always()")
|
||||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||||
Conclusion: model.StepStatusFailure,
|
Conclusion: model.StepStatusFailure,
|
||||||
}
|
}
|
||||||
assertObject.True(isStepEnabled(context.Background(), step))
|
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||||
}
|
}
|
||||||
|
|
7
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/action.yml
vendored
Normal file
7
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/action.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: "last action check"
|
||||||
|
description: "last action check"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "node16"
|
||||||
|
main: main.js
|
||||||
|
post: post.js
|
0
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/main.js
vendored
Normal file
0
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/main.js
vendored
Normal file
17
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/post.js
vendored
Normal file
17
pkg/runner/testdata/uses-action-with-pre-and-post-step/last-action/post.js
vendored
Normal 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}'`);
|
||||||
|
}
|
15
pkg/runner/testdata/uses-action-with-pre-and-post-step/push.yml
vendored
Normal file
15
pkg/runner/testdata/uses-action-with-pre-and-post-step/push.yml
vendored
Normal 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
|
13
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/action.yml
vendored
Normal file
13
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/action.yml
vendored
Normal 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
|
3
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/main.js
vendored
Normal file
3
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/main.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const { appendFileSync } = require('fs');
|
||||||
|
const step = process.env['INPUT_STEP'];
|
||||||
|
appendFileSync(process.env['GITHUB_ENV'], `;${step}`, { encoding:'utf-8' })
|
3
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/post.js
vendored
Normal file
3
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/post.js
vendored
Normal 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' })
|
1
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/pre.js
vendored
Normal file
1
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/action-with-pre-and-post/pre.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log('pre');
|
12
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/composite_action/action.yml
vendored
Normal file
12
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/composite_action/action.yml
vendored
Normal 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
|
7
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/action.yml
vendored
Normal file
7
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/action.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name: "last action check"
|
||||||
|
description: "last action check"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "node16"
|
||||||
|
main: main.js
|
||||||
|
post: post.js
|
0
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/main.js
vendored
Normal file
0
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/main.js
vendored
Normal file
7
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/post.js
vendored
Normal file
7
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/last-action/post.js
vendored
Normal 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}'`);
|
||||||
|
}
|
11
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/push.yml
vendored
Normal file
11
pkg/runner/testdata/uses-composite-with-pre-and-post-steps/push.yml
vendored
Normal 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
|
Loading…
Reference in a new issue