github.com/nektos/act@v0.2.63/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,gocyclo 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 for i, stepModel := range infoSteps { 57 stepModel := stepModel 58 if stepModel == nil { 59 return func(ctx context.Context) error { 60 return fmt.Errorf("invalid Step %v: missing run or uses key", i) 61 } 62 } 63 if stepModel.ID == "" { 64 stepModel.ID = fmt.Sprintf("%d", i) 65 } 66 67 step, err := sf.newStep(stepModel, rc) 68 69 if err != nil { 70 return common.NewErrorExecutor(err) 71 } 72 73 preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, step.pre())) 74 75 stepExec := step.main() 76 steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error { 77 logger := common.Logger(ctx) 78 err := stepExec(ctx) 79 if err != nil { 80 logger.Errorf("%v", err) 81 common.SetJobError(ctx, err) 82 } else if ctx.Err() != nil { 83 logger.Errorf("%v", ctx.Err()) 84 common.SetJobError(ctx, ctx.Err()) 85 } 86 return nil 87 })) 88 89 postExec := useStepLogger(rc, stepModel, stepStagePost, step.post()) 90 if postExecutor != nil { 91 // run the post executor in reverse order 92 postExecutor = postExec.Finally(postExecutor) 93 } else { 94 postExecutor = postExec 95 } 96 } 97 98 postExecutor = postExecutor.Finally(func(ctx context.Context) error { 99 jobError := common.JobError(ctx) 100 var err error 101 if rc.Config.AutoRemove || jobError == nil { 102 // always allow 1 min for stopping and removing the runner, even if we were cancelled 103 ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute) 104 defer cancel() 105 106 logger := common.Logger(ctx) 107 logger.Infof("Cleaning up container for job %s", rc.JobName) 108 if err = info.stopContainer()(ctx); err != nil { 109 logger.Errorf("Error while stop job container: %v", err) 110 } 111 } 112 setJobResult(ctx, info, rc, jobError == nil) 113 setJobOutputs(ctx, rc) 114 115 return err 116 }) 117 118 pipeline := make([]common.Executor, 0) 119 pipeline = append(pipeline, preSteps...) 120 pipeline = append(pipeline, steps...) 121 122 return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...). 123 Finally(func(ctx context.Context) error { //nolint:contextcheck 124 var cancel context.CancelFunc 125 if ctx.Err() == context.Canceled { 126 // in case of an aborted run, we still should execute the 127 // post steps to allow cleanup. 128 ctx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), 5*time.Minute) 129 defer cancel() 130 } 131 return postExecutor(ctx) 132 }). 133 Finally(info.interpolateOutputs()). 134 Finally(info.closeContainer())) 135 } 136 137 func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) { 138 logger := common.Logger(ctx) 139 140 jobResult := "success" 141 // we have only one result for a whole matrix build, so we need 142 // to keep an existing result state if we run a matrix 143 if len(info.matrix()) > 0 && rc.Run.Job().Result != "" { 144 jobResult = rc.Run.Job().Result 145 } 146 147 if !success { 148 jobResult = "failure" 149 } 150 151 info.result(jobResult) 152 if rc.caller != nil { 153 // set reusable workflow job result 154 rc.caller.runContext.result(jobResult) 155 } 156 157 jobResultMessage := "succeeded" 158 if jobResult != "success" { 159 jobResultMessage = "failed" 160 } 161 162 logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage) 163 } 164 165 func setJobOutputs(ctx context.Context, rc *RunContext) { 166 if rc.caller != nil { 167 // map outputs for reusable workflows 168 callerOutputs := make(map[string]string) 169 170 ee := rc.NewExpressionEvaluator(ctx) 171 172 for k, v := range rc.Run.Workflow.WorkflowCallConfig().Outputs { 173 callerOutputs[k] = ee.Interpolate(ctx, ee.Interpolate(ctx, v.Value)) 174 } 175 176 rc.caller.runContext.Run.Job().Outputs = callerOutputs 177 } 178 } 179 180 func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor { 181 return func(ctx context.Context) error { 182 ctx = withStepLogger(ctx, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) 183 184 rawLogger := common.Logger(ctx).WithField("raw_output", true) 185 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { 186 if rc.Config.LogOutput { 187 rawLogger.Infof("%s", s) 188 } else { 189 rawLogger.Debugf("%s", s) 190 } 191 return true 192 }) 193 194 oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter) 195 defer rc.JobContainer.ReplaceLogWriter(oldout, olderr) 196 197 return executor(ctx) 198 } 199 }