package runner import ( "context" "fmt" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/model" ) func evaluteCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) (inputs map[string]interface{}, env map[string]string) { eval := parent.NewExpressionEvaluator(ctx) inputs = make(map[string]interface{}) for k, input := range step.getActionModel().Inputs { inputs[k] = eval.Interpolate(ctx, input.Default) } if step.getStepModel().With != nil { for k, v := range step.getStepModel().With { inputs[k] = eval.Interpolate(ctx, v) } } env = make(map[string]string) for k, v := range parent.Env { env[k] = eval.Interpolate(ctx, v) } for k, v := range step.getStepModel().Environment() { env[k] = eval.Interpolate(ctx, v) } return inputs, env } func newCompositeRunContext(ctx context.Context, parent *RunContext, step actionStep, actionPath string) *RunContext { inputs, env := evaluteCompositeInputAndEnv(ctx, 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(ctx context.Context, parent *RunContext, step actionStep) { inputs, env := evaluteCompositeInputAndEnv(ctx, 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(ctx) steps := step.getCompositeSteps() ctx = WithCompositeLogger(ctx, &compositerc.Masks) compositerc.updateCompositeRunContext(ctx, rc, step) err := steps.main(ctx) // Map outputs from composite RunContext to job RunContext eval := compositerc.NewExpressionEvaluator(ctx) for outputName, output := range action.Outputs { rc.setOutput(ctx, map[string]string{ "name": outputName, }, eval.Interpolate(ctx, 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 { logger := common.Logger(ctx) err := step.main()(ctx) if err != nil { logger.Errorf("%v", err) common.SetJobError(ctx, err) } else if ctx.Err() != nil { logger.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) } }