github.com/nektos/act@v0.2.63/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 } 78 compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx) 79 80 return compositerc 81 } 82 83 func execAsComposite(step actionStep) common.Executor { 84 rc := step.getRunContext() 85 action := step.getActionModel() 86 87 return func(ctx context.Context) error { 88 compositeRC := step.getCompositeRunContext(ctx) 89 90 steps := step.getCompositeSteps() 91 92 if steps == nil || steps.main == nil { 93 return fmt.Errorf("missing steps in composite action") 94 } 95 96 ctx = WithCompositeLogger(ctx, &compositeRC.Masks) 97 98 err := steps.main(ctx) 99 100 // Map outputs from composite RunContext to job RunContext 101 eval := compositeRC.NewExpressionEvaluator(ctx) 102 for outputName, output := range action.Outputs { 103 rc.setOutput(ctx, map[string]string{ 104 "name": outputName, 105 }, eval.Interpolate(ctx, output.Value)) 106 } 107 108 rc.Masks = append(rc.Masks, compositeRC.Masks...) 109 rc.ExtraPath = compositeRC.ExtraPath 110 // compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv 111 mergeIntoMap := mergeIntoMapCaseSensitive 112 if rc.JobContainer.IsEnvironmentCaseInsensitive() { 113 mergeIntoMap = mergeIntoMapCaseInsensitive 114 } 115 if rc.GlobalEnv == nil { 116 rc.GlobalEnv = map[string]string{} 117 } 118 mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv) 119 mergeIntoMap(rc.Env, compositeRC.GlobalEnv) 120 121 return err 122 } 123 } 124 125 type compositeSteps struct { 126 pre common.Executor 127 main common.Executor 128 post common.Executor 129 } 130 131 // Executor returns a pipeline executor for all the steps in the job 132 func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps { 133 steps := make([]common.Executor, 0) 134 preSteps := make([]common.Executor, 0) 135 var postExecutor common.Executor 136 137 sf := &stepFactoryImpl{} 138 139 for i, step := range action.Runs.Steps { 140 if step.ID == "" { 141 step.ID = fmt.Sprintf("%d", i) 142 } 143 144 // create a copy of the step, since this composite action could 145 // run multiple times and we might modify the instance 146 stepcopy := step 147 148 step, err := sf.newStep(&stepcopy, rc) 149 if err != nil { 150 return &compositeSteps{ 151 main: common.NewErrorExecutor(err), 152 } 153 } 154 155 stepID := step.getStepModel().ID 156 stepPre := rc.newCompositeCommandExecutor(step.pre()) 157 preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID)) 158 159 steps = append(steps, func(ctx context.Context) error { 160 ctx = WithCompositeStepLogger(ctx, stepID) 161 logger := common.Logger(ctx) 162 err := rc.newCompositeCommandExecutor(step.main())(ctx) 163 164 if err != nil { 165 logger.Errorf("%v", err) 166 common.SetJobError(ctx, err) 167 } else if ctx.Err() != nil { 168 logger.Errorf("%v", ctx.Err()) 169 common.SetJobError(ctx, ctx.Err()) 170 } 171 return nil 172 }) 173 174 // run the post executor in reverse order 175 if postExecutor != nil { 176 stepPost := rc.newCompositeCommandExecutor(step.post()) 177 postExecutor = newCompositeStepLogExecutor(stepPost.Finally(postExecutor), stepID) 178 } else { 179 stepPost := rc.newCompositeCommandExecutor(step.post()) 180 postExecutor = newCompositeStepLogExecutor(stepPost, stepID) 181 } 182 } 183 184 steps = append(steps, common.JobError) 185 return &compositeSteps{ 186 pre: func(ctx context.Context) error { 187 return common.NewPipelineExecutor(preSteps...)(common.WithJobErrorContainer(ctx)) 188 }, 189 main: func(ctx context.Context) error { 190 return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx)) 191 }, 192 post: postExecutor, 193 } 194 } 195 196 func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor { 197 return func(ctx context.Context) error { 198 ctx = WithCompositeLogger(ctx, &rc.Masks) 199 200 // We need to inject a composite RunContext related command 201 // handler into the current running job container 202 // We need this, to support scoping commands to the composite action 203 // executing. 204 rawLogger := common.Logger(ctx).WithField("raw_output", true) 205 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { 206 if rc.Config.LogOutput { 207 rawLogger.Infof("%s", s) 208 } else { 209 rawLogger.Debugf("%s", s) 210 } 211 return true 212 }) 213 214 oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter) 215 defer rc.JobContainer.ReplaceLogWriter(oldout, olderr) 216 217 return executor(ctx) 218 } 219 } 220 221 func newCompositeStepLogExecutor(runStep common.Executor, stepID string) common.Executor { 222 return func(ctx context.Context) error { 223 ctx = WithCompositeStepLogger(ctx, stepID) 224 logger := common.Logger(ctx) 225 err := runStep(ctx) 226 if err != nil { 227 logger.Errorf("%v", err) 228 common.SetJobError(ctx, err) 229 } else if ctx.Err() != nil { 230 logger.Errorf("%v", ctx.Err()) 231 common.SetJobError(ctx, ctx.Err()) 232 } 233 return nil 234 } 235 }