github.com/nektos/act@v0.2.83/pkg/runner/job_executor.go (about) 1 package runner 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/nektos/act/pkg/common" 9 "github.com/nektos/act/pkg/model" 10 ) 11 12 type jobInfo interface { 13 matrix() map[string]interface{} 14 steps() []*model.Step 15 startContainer() common.Executor 16 stopContainer() common.Executor 17 closeContainer() common.Executor 18 interpolateOutputs() common.Executor 19 result(result string) 20 } 21 22 //nolint:contextcheck 23 func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor { 24 steps := make([]common.Executor, 0) 25 preSteps := make([]common.Executor, 0) 26 var postExecutor common.Executor 27 28 steps = append(steps, func(ctx context.Context) error { 29 logger := common.Logger(ctx) 30 if len(info.matrix()) > 0 { 31 logger.Infof("\U0001F9EA Matrix: %v", info.matrix()) 32 } 33 return nil 34 }) 35 36 infoSteps := info.steps() 37 38 if len(infoSteps) == 0 { 39 return common.NewDebugExecutor("No steps found") 40 } 41 42 preSteps = append(preSteps, func(ctx context.Context) error { 43 // Have to be skipped for some Tests 44 if rc.Run == nil { 45 return nil 46 } 47 rc.ExprEval = rc.NewExpressionEvaluator(ctx) 48 // evaluate environment variables since they can contain 49 // GitHub's special environment variables. 50 for k, v := range rc.GetEnv() { 51 rc.Env[k] = rc.ExprEval.Interpolate(ctx, v) 52 } 53 return nil 54 }) 55 56 var setJobError = func(ctx context.Context, err error) error { 57 if err == nil { 58 return nil 59 } 60 logger := common.Logger(ctx) 61 logger.Errorf("%v", err) 62 common.SetJobError(ctx, err) 63 return err 64 } 65 66 for i, stepModel := range infoSteps { 67 if stepModel == nil { 68 return func(_ context.Context) error { 69 return fmt.Errorf("invalid Step %v: missing run or uses key", i) 70 } 71 } 72 if stepModel.ID == "" { 73 stepModel.ID = fmt.Sprintf("%d", i) 74 } 75 76 step, err := sf.newStep(stepModel, rc) 77 78 if err != nil { 79 return common.NewErrorExecutor(err) 80 } 81 82 preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, step.pre().ThenError(setJobError))) 83 84 stepExec := step.main() 85 steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error { 86 err := stepExec(ctx) 87 if err != nil { 88 _ = setJobError(ctx, err) 89 } else if ctx.Err() != nil { 90 _ = setJobError(ctx, ctx.Err()) 91 } 92 return nil 93 })) 94 95 postExec := useStepLogger(rc, stepModel, stepStagePost, step.post().ThenError(setJobError)) 96 if postExecutor != nil { 97 // run the post executor in reverse order 98 postExecutor = postExec.Finally(postExecutor) 99 } else { 100 postExecutor = postExec 101 } 102 } 103 104 var stopContainerExecutor common.Executor = func(ctx context.Context) error { 105 jobError := common.JobError(ctx) 106 var err error 107 if rc.Config.AutoRemove || jobError == nil { 108 // always allow 1 min for stopping and removing the runner, even if we were cancelled 109 ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute) 110 defer cancel() 111 112 logger := common.Logger(ctx) 113 logger.Infof("Cleaning up container for job %s", rc.JobName) 114 if err = info.stopContainer()(ctx); err != nil { 115 logger.Errorf("Error while stop job container: %v", err) 116 } 117 } 118 return err 119 } 120 121 var setJobResultExecutor common.Executor = func(ctx context.Context) error { 122 jobError := common.JobError(ctx) 123 setJobResult(ctx, info, rc, jobError == nil) 124 setJobOutputs(ctx, rc) 125 return nil 126 } 127 128 pipeline := make([]common.Executor, 0) 129 pipeline = append(pipeline, preSteps...) 130 pipeline = append(pipeline, steps...) 131 132 return common.NewPipelineExecutor( 133 common.NewFieldExecutor("step", "Set up job", common.NewFieldExecutor("stepid", []string{"--setup-job"}, 134 common.NewPipelineExecutor(common.NewInfoExecutor("\u2B50 Run Set up job"), info.startContainer(), rc.InitializeNodeTool()). 135 Then(common.NewFieldExecutor("stepResult", model.StepStatusSuccess, common.NewInfoExecutor(" \u2705 Success - Set up job"))). 136 ThenError(setJobError).OnError(common.NewFieldExecutor("stepResult", model.StepStatusFailure, common.NewInfoExecutor(" \u274C Failure - Set up job"))))), 137 common.NewPipelineExecutor(pipeline...). 138 Finally(func(ctx context.Context) error { //nolint:contextcheck 139 var cancel context.CancelFunc 140 if ctx.Err() == context.Canceled { 141 // in case of an aborted run, we still should execute the 142 // post steps to allow cleanup. 143 ctx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), 5*time.Minute) 144 defer cancel() 145 } 146 return postExecutor(ctx) 147 }). 148 Finally(common.NewFieldExecutor("step", "Complete job", common.NewFieldExecutor("stepid", []string{"--complete-job"}, 149 common.NewInfoExecutor("\u2B50 Run Complete job"). 150 Finally(stopContainerExecutor). 151 Finally( 152 info.interpolateOutputs().Finally(info.closeContainer()).Then(common.NewFieldExecutor("stepResult", model.StepStatusSuccess, common.NewInfoExecutor(" \u2705 Success - Complete job"))). 153 OnError(common.NewFieldExecutor("stepResult", model.StepStatusFailure, common.NewInfoExecutor(" \u274C Failure - Complete job"))), 154 ))))).Finally(setJobResultExecutor) 155 } 156 157 func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) { 158 logger := common.Logger(ctx) 159 160 jobResult := "success" 161 // we have only one result for a whole matrix build, so we need 162 // to keep an existing result state if we run a matrix 163 if len(info.matrix()) > 0 && rc.Run.Job().Result != "" { 164 jobResult = rc.Run.Job().Result 165 } 166 167 if !success { 168 jobResult = "failure" 169 } 170 171 info.result(jobResult) 172 if rc.caller != nil { 173 // set reusable workflow job result 174 rc.caller.runContext.result(jobResult) 175 } 176 177 jobResultMessage := "succeeded" 178 if jobResult != "success" { 179 jobResultMessage = "failed" 180 } 181 182 logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage) 183 } 184 185 func setJobOutputs(ctx context.Context, rc *RunContext) { 186 if rc.caller != nil { 187 // map outputs for reusable workflows 188 callerOutputs := make(map[string]string) 189 190 ee := rc.NewExpressionEvaluator(ctx) 191 192 for k, v := range rc.Run.Workflow.WorkflowCallConfig().Outputs { 193 callerOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value)) 194 } 195 196 rc.caller.runContext.Run.Job().Outputs = callerOutputs 197 } 198 } 199 200 func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor { 201 return func(ctx context.Context) error { 202 ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) 203 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 }