github.com/nektos/act@v0.2.83/pkg/runner/action_composite.go (about) 1 package runner 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/nektos/act/pkg/common" 10 "github.com/nektos/act/pkg/model" 11 ) 12 13 func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string { 14 env := make(map[string]string) 15 stepEnv := *step.getEnv() 16 for k, v := range stepEnv { 17 // do not set current inputs into composite action 18 // the required inputs are added in the second loop 19 if !strings.HasPrefix(k, "INPUT_") { 20 env[k] = v 21 } 22 } 23 24 ee := parent.NewStepExpressionEvaluator(ctx, step) 25 26 for inputID, input := range step.getActionModel().Inputs { 27 envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_") 28 envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey)) 29 30 // lookup if key is defined in the step but the already 31 // evaluated value from the environment 32 _, defined := step.getStepModel().With[inputID] 33 if value, ok := stepEnv[envKey]; defined && ok { 34 env[envKey] = value 35 } else { 36 // defaults could contain expressions 37 env[envKey] = ee.Interpolate(ctx, input.Default) 38 } 39 } 40 gh := step.getGithubContext(ctx) 41 env["GITHUB_ACTION_REPOSITORY"] = gh.ActionRepository 42 env["GITHUB_ACTION_REF"] = gh.ActionRef 43 44 return env 45 } 46 47 func newCompositeRunContext(ctx context.Context, parent *RunContext, step actionStep, actionPath string) *RunContext { 48 env := evaluateCompositeInputAndEnv(ctx, parent, step) 49 50 // run with the global config but without secrets 51 configCopy := *(parent.Config) 52 configCopy.Secrets = nil 53 54 // create a run context for the composite action to run in 55 compositerc := &RunContext{ 56 Name: parent.Name, 57 JobName: parent.JobName, 58 Run: &model.Run{ 59 JobID: parent.Run.JobID, 60 Workflow: &model.Workflow{ 61 Name: parent.Run.Workflow.Name, 62 Jobs: map[string]*model.Job{ 63 parent.Run.JobID: {}, 64 }, 65 }, 66 }, 67 Config: &configCopy, 68 StepResults: map[string]*model.StepResult{}, 69 JobContainer: parent.JobContainer, 70 ActionPath: actionPath, 71 Env: env, 72 GlobalEnv: parent.GlobalEnv, 73 Masks: parent.Masks, 74 ExtraPath: parent.ExtraPath, 75 Parent: parent, 76 EventJSON: parent.EventJSON, 77 nodeToolFullPath: parent.nodeToolFullPath, 78 } 79 compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx) 80 81 return compositerc 82 } 83 84 func execAsComposite(step actionStep) common.Executor { 85 rc := step.getRunContext() 86 action := step.getActionModel() 87 88 return func(ctx context.Context) error { 89 compositeRC := step.getCompositeRunContext(ctx) 90 91 steps := step.getCompositeSteps() 92 93 if steps == nil || steps.main == nil { 94 return fmt.Errorf("missing steps in composite action") 95 } 96 97 ctx = WithCompositeLogger(ctx, &compositeRC.Masks) 98 99 err := steps.main(ctx) 100 101 // Map outputs from composite RunContext to job RunContext 102 eval := compositeRC.NewExpressionEvaluator(ctx) 103 for outputName, output := range action.Outputs { 104 rc.setOutput(ctx, map[string]string{ 105 "name": outputName, 106 }, eval.Interpolate(ctx, output.Value)) 107 } 108 109 rc.Masks = append(rc.Masks, compositeRC.Masks...) 110 rc.ExtraPath = compositeRC.ExtraPath 111 // compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv 112 mergeIntoMap := mergeIntoMapCaseSensitive 113 if rc.JobContainer.IsEnvironmentCaseInsensitive() { 114 mergeIntoMap = mergeIntoMapCaseInsensitive 115 } 116 if rc.GlobalEnv == nil { 117 rc.GlobalEnv = map[string]string{} 118 } 119 mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv) 120 mergeIntoMap(rc.Env, compositeRC.GlobalEnv) 121 122 return err 123 } 124 } 125 126 type compositeSteps struct { 127 pre common.Executor 128 main common.Executor 129 post common.Executor 130 } 131 132 // Executor returns a pipeline executor for all the steps in the job 133 func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps { 134 steps := make([]common.Executor, 0) 135 preSteps := make([]common.Executor, 0) 136 var postExecutor common.Executor 137 138 sf := &stepFactoryImpl{} 139 140 for i, step := range action.Runs.Steps { 141 if step.ID == "" { 142 step.ID = fmt.Sprintf("%d", i) 143 } 144 145 // create a copy of the step, since this composite action could 146 // run multiple times and we might modify the instance 147 stepcopy := step 148 149 step, err := sf.newStep(&stepcopy, rc) 150 if err != nil { 151 return &compositeSteps{ 152 main: common.NewErrorExecutor(err), 153 } 154 } 155 156 stepID := step.getStepModel().ID 157 stepPre := rc.newCompositeCommandExecutor(step.pre()) 158 preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID)) 159 160 steps = append(steps, func(ctx context.Context) error { 161 ctx = WithCompositeStepLogger(ctx, stepID) 162 logger := common.Logger(ctx) 163 err := rc.newCompositeCommandExecutor(step.main())(ctx) 164 165 if err != nil { 166 logger.Errorf("%v", err) 167 common.SetJobError(ctx, err) 168 } else if ctx.Err() != nil { 169 logger.Errorf("%v", ctx.Err()) 170 common.SetJobError(ctx, ctx.Err()) 171 } 172 return nil 173 }) 174 175 // run the post executor in reverse order 176 if postExecutor != nil { 177 stepPost := rc.newCompositeCommandExecutor(step.post()) 178 postExecutor = newCompositeStepLogExecutor(stepPost.Finally(postExecutor), stepID) 179 } else { 180 stepPost := rc.newCompositeCommandExecutor(step.post()) 181 postExecutor = newCompositeStepLogExecutor(stepPost, stepID) 182 } 183 } 184 185 steps = append(steps, common.JobError) 186 return &compositeSteps{ 187 pre: func(ctx context.Context) error { 188 return common.NewPipelineExecutor(preSteps...)(common.WithJobErrorContainer(ctx)) 189 }, 190 main: func(ctx context.Context) error { 191 return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx)) 192 }, 193 post: postExecutor, 194 } 195 } 196 197 func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor { 198 return func(ctx context.Context) error { 199 ctx = WithCompositeLogger(ctx, &rc.Masks) 200 201 // We need to inject a composite RunContext related command 202 // handler into the current running job container 203 // We need this, to support scoping commands to the composite action 204 // executing. 205 rawLogger := common.Logger(ctx).WithField("raw_output", true) 206 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { 207 if rc.Config.LogOutput { 208 rawLogger.Infof("%s", s) 209 } else { 210 rawLogger.Debugf("%s", s) 211 } 212 return true 213 }) 214 215 oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter) 216 defer rc.JobContainer.ReplaceLogWriter(oldout, olderr) 217 218 return executor(ctx) 219 } 220 } 221 222 func newCompositeStepLogExecutor(runStep common.Executor, stepID string) common.Executor { 223 return func(ctx context.Context) error { 224 ctx = WithCompositeStepLogger(ctx, stepID) 225 logger := common.Logger(ctx) 226 err := runStep(ctx) 227 if err != nil { 228 logger.Errorf("%v", err) 229 common.SetJobError(ctx, err) 230 } else if ctx.Err() != nil { 231 logger.Errorf("%v", ctx.Err()) 232 common.SetJobError(ctx, ctx.Err()) 233 } 234 return nil 235 } 236 }