act/pkg/runner/action_composite.go

196 lines
5.3 KiB
Go
Raw Normal View History

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
2022-05-24 08:36:06 -05:00
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)
}
}