github.com/nektos/act@v0.2.83/pkg/runner/step.go (about) 1 package runner 2 3 import ( 4 "archive/tar" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "path" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/nektos/act/pkg/common" 15 "github.com/nektos/act/pkg/container" 16 "github.com/nektos/act/pkg/exprparser" 17 "github.com/nektos/act/pkg/model" 18 "github.com/sirupsen/logrus" 19 ) 20 21 type step interface { 22 pre() common.Executor 23 main() common.Executor 24 post() common.Executor 25 26 getRunContext() *RunContext 27 getGithubContext(ctx context.Context) *model.GithubContext 28 getStepModel() *model.Step 29 getEnv() *map[string]string 30 getIfExpression(context context.Context, stage stepStage) string 31 } 32 33 type stepStage int 34 35 const ( 36 stepStagePre stepStage = iota 37 stepStageMain 38 stepStagePost 39 ) 40 41 // Controls how many symlinks are resolved for local and remote Actions 42 const maxSymlinkDepth = 10 43 44 func (s stepStage) String() string { 45 switch s { 46 case stepStagePre: 47 return "Pre" 48 case stepStageMain: 49 return "Main" 50 case stepStagePost: 51 return "Post" 52 } 53 return "Unknown" 54 } 55 56 func processRunnerSummaryCommand(ctx context.Context, fileName string, rc *RunContext) error { 57 if common.Dryrun(ctx) { 58 return nil 59 } 60 pathTar, err := rc.JobContainer.GetContainerArchive(ctx, path.Join(rc.JobContainer.GetActPath(), fileName)) 61 if err != nil { 62 return err 63 } 64 defer pathTar.Close() 65 66 reader := tar.NewReader(pathTar) 67 _, err = reader.Next() 68 if err != nil && err != io.EOF { 69 return err 70 } 71 summary, err := io.ReadAll(reader) 72 if err != nil { 73 return err 74 } 75 if len(summary) == 0 { 76 return nil 77 } 78 common.Logger(ctx).WithFields(logrus.Fields{"command": "summary", "content": string(summary)}).Infof(" \U00002699 Summary - %s", string(summary)) 79 return nil 80 } 81 82 func processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error { 83 env := map[string]string{} 84 err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx) 85 if err != nil { 86 return err 87 } 88 for k, v := range env { 89 setter(ctx, map[string]string{"name": k}, v) 90 } 91 return nil 92 } 93 94 func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor { 95 return func(ctx context.Context) error { 96 logger := common.Logger(ctx) 97 rc := step.getRunContext() 98 stepModel := step.getStepModel() 99 100 ifExpression := step.getIfExpression(ctx, stage) 101 rc.CurrentStep = stepModel.ID 102 103 stepResult := &model.StepResult{ 104 Outcome: model.StepStatusSuccess, 105 Conclusion: model.StepStatusSuccess, 106 Outputs: make(map[string]string), 107 } 108 if stage == stepStageMain { 109 rc.StepResults[rc.CurrentStep] = stepResult 110 } 111 112 err := setupEnv(ctx, step) 113 if err != nil { 114 return err 115 } 116 117 cctx := common.JobCancelContext(ctx) 118 rc.Cancelled = cctx != nil && cctx.Err() != nil 119 120 runStep, err := isStepEnabled(ctx, ifExpression, step, stage) 121 if err != nil { 122 stepResult.Conclusion = model.StepStatusFailure 123 stepResult.Outcome = model.StepStatusFailure 124 return err 125 } 126 127 if !runStep { 128 stepResult.Conclusion = model.StepStatusSkipped 129 stepResult.Outcome = model.StepStatusSkipped 130 logger.WithField("stepResult", stepResult.Outcome).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression) 131 return nil 132 } 133 134 stepString := rc.ExprEval.Interpolate(ctx, stepModel.String()) 135 if strings.Contains(stepString, "::add-mask::") { 136 stepString = "add-mask command" 137 } 138 logger.Infof("\u2B50 Run %s %s", stage, stepString) 139 140 // Prepare and clean Runner File Commands 141 actPath := rc.JobContainer.GetActPath() 142 143 outputFileCommand := path.Join("workflow", "outputcmd.txt") 144 (*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand) 145 146 stateFileCommand := path.Join("workflow", "statecmd.txt") 147 (*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand) 148 149 pathFileCommand := path.Join("workflow", "pathcmd.txt") 150 (*step.getEnv())["GITHUB_PATH"] = path.Join(actPath, pathFileCommand) 151 152 envFileCommand := path.Join("workflow", "envs.txt") 153 (*step.getEnv())["GITHUB_ENV"] = path.Join(actPath, envFileCommand) 154 155 summaryFileCommand := path.Join("workflow", "SUMMARY.md") 156 (*step.getEnv())["GITHUB_STEP_SUMMARY"] = path.Join(actPath, summaryFileCommand) 157 158 _ = rc.JobContainer.Copy(actPath, &container.FileEntry{ 159 Name: outputFileCommand, 160 Mode: 0o666, 161 }, &container.FileEntry{ 162 Name: stateFileCommand, 163 Mode: 0o666, 164 }, &container.FileEntry{ 165 Name: pathFileCommand, 166 Mode: 0o666, 167 }, &container.FileEntry{ 168 Name: envFileCommand, 169 Mode: 0666, 170 }, &container.FileEntry{ 171 Name: summaryFileCommand, 172 Mode: 0o666, 173 })(ctx) 174 175 stepCtx, cancelStepCtx := context.WithCancel(ctx) 176 defer cancelStepCtx() 177 var cancelTimeOut context.CancelFunc 178 stepCtx, cancelTimeOut = evaluateStepTimeout(stepCtx, rc.ExprEval, stepModel) 179 defer cancelTimeOut() 180 monitorJobCancellation(ctx, stepCtx, cctx, rc, logger, ifExpression, step, stage, cancelStepCtx) 181 startTime := time.Now() 182 err = executor(stepCtx) 183 executionTime := time.Since(startTime) 184 185 if err == nil { 186 logger.WithFields(logrus.Fields{"executionTime": executionTime, "stepResult": stepResult.Outcome}).Infof(" \u2705 Success - %s %s [%s]", stage, stepString, executionTime) 187 } else { 188 stepResult.Outcome = model.StepStatusFailure 189 190 continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage) 191 if parseErr != nil { 192 stepResult.Conclusion = model.StepStatusFailure 193 return parseErr 194 } 195 196 if continueOnError { 197 logger.Infof("Failed but continue next step") 198 err = nil 199 stepResult.Conclusion = model.StepStatusSuccess 200 } else { 201 stepResult.Conclusion = model.StepStatusFailure 202 } 203 204 logger.WithFields(logrus.Fields{"executionTime": executionTime, "stepResult": stepResult.Outcome}).Infof(" \u274C Failure - %s %s [%s]", stage, stepString, executionTime) 205 } 206 // Process Runner File Commands 207 ferrors := []error{err} 208 ferrors = append(ferrors, processRunnerEnvFileCommand(ctx, envFileCommand, rc, rc.setEnv)) 209 ferrors = append(ferrors, processRunnerEnvFileCommand(ctx, stateFileCommand, rc, rc.saveState)) 210 ferrors = append(ferrors, processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput)) 211 ferrors = append(ferrors, processRunnerSummaryCommand(ctx, summaryFileCommand, rc)) 212 ferrors = append(ferrors, rc.UpdateExtraPath(ctx, path.Join(actPath, pathFileCommand))) 213 return errors.Join(ferrors...) 214 } 215 } 216 217 func monitorJobCancellation(ctx context.Context, stepCtx context.Context, jobCancellationCtx context.Context, rc *RunContext, logger logrus.FieldLogger, ifExpression string, step step, stage stepStage, cancelStepCtx context.CancelFunc) { 218 if !rc.Cancelled && jobCancellationCtx != nil { 219 go func() { 220 select { 221 case <-jobCancellationCtx.Done(): 222 rc.Cancelled = true 223 logger.Infof("Reevaluate condition %v due to cancellation", ifExpression) 224 keepStepRunning, err := isStepEnabled(ctx, ifExpression, step, stage) 225 logger.Infof("Result condition keepStepRunning=%v", keepStepRunning) 226 if !keepStepRunning || err != nil { 227 cancelStepCtx() 228 } 229 case <-stepCtx.Done(): 230 } 231 }() 232 } 233 } 234 235 func evaluateStepTimeout(ctx context.Context, exprEval ExpressionEvaluator, stepModel *model.Step) (context.Context, context.CancelFunc) { 236 timeout := exprEval.Interpolate(ctx, stepModel.TimeoutMinutes) 237 if timeout != "" { 238 if timeOutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil { 239 return context.WithTimeout(ctx, time.Duration(timeOutMinutes)*time.Minute) 240 } 241 } 242 return ctx, func() {} 243 } 244 245 func setupEnv(ctx context.Context, step step) error { 246 rc := step.getRunContext() 247 248 mergeEnv(ctx, step) 249 // merge step env last, since it should not be overwritten 250 mergeIntoMap(step, step.getEnv(), step.getStepModel().GetEnv()) 251 252 exprEval := rc.NewExpressionEvaluator(ctx) 253 for k, v := range *step.getEnv() { 254 if !strings.HasPrefix(k, "INPUT_") { 255 (*step.getEnv())[k] = exprEval.Interpolate(ctx, v) 256 } 257 } 258 // after we have an evaluated step context, update the expressions evaluator with a new env context 259 // you can use step level env in the with property of a uses construct 260 exprEval = rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv()) 261 for k, v := range *step.getEnv() { 262 if strings.HasPrefix(k, "INPUT_") { 263 (*step.getEnv())[k] = exprEval.Interpolate(ctx, v) 264 } 265 } 266 267 common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv()) 268 269 return nil 270 } 271 272 func mergeEnv(ctx context.Context, step step) { 273 env := step.getEnv() 274 rc := step.getRunContext() 275 job := rc.Run.Job() 276 277 c := job.Container() 278 if c != nil { 279 mergeIntoMap(step, env, rc.GetEnv(), c.Env) 280 } else { 281 mergeIntoMap(step, env, rc.GetEnv()) 282 } 283 284 rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env) 285 286 if step.getStepModel().Uses != "" { 287 // prevent uses action input pollution of unset parameters, skip this for run steps 288 // due to design flaw 289 for key := range *env { 290 if strings.Contains(key, "INPUT_") { 291 delete(*env, key) 292 } 293 } 294 } 295 } 296 297 func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage) (bool, error) { 298 rc := step.getRunContext() 299 300 var defaultStatusCheck exprparser.DefaultStatusCheck 301 if stage == stepStagePost { 302 defaultStatusCheck = exprparser.DefaultStatusCheckAlways 303 } else { 304 defaultStatusCheck = exprparser.DefaultStatusCheckSuccess 305 } 306 307 runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluatorExt(ctx, step, stage == stepStageMain), expr, defaultStatusCheck) 308 if err != nil { 309 return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err) 310 } 311 312 return runStep, nil 313 } 314 315 func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage) (bool, error) { 316 // https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=true#L962 317 if len(strings.TrimSpace(expr)) == 0 { 318 return false, nil 319 } 320 321 rc := step.getRunContext() 322 323 continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone) 324 if err != nil { 325 return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err) 326 } 327 328 return continueOnError, nil 329 } 330 331 func mergeIntoMap(step step, target *map[string]string, maps ...map[string]string) { 332 if rc := step.getRunContext(); rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive() { 333 mergeIntoMapCaseInsensitive(*target, maps...) 334 } else { 335 mergeIntoMapCaseSensitive(*target, maps...) 336 } 337 } 338 339 func mergeIntoMapCaseSensitive(target map[string]string, maps ...map[string]string) { 340 for _, m := range maps { 341 for k, v := range m { 342 target[k] = v 343 } 344 } 345 } 346 347 func mergeIntoMapCaseInsensitive(target map[string]string, maps ...map[string]string) { 348 foldKeys := make(map[string]string, len(target)) 349 for k := range target { 350 foldKeys[strings.ToLower(k)] = k 351 } 352 toKey := func(s string) string { 353 foldKey := strings.ToLower(s) 354 if k, ok := foldKeys[foldKey]; ok { 355 return k 356 } 357 foldKeys[strings.ToLower(foldKey)] = s 358 return s 359 } 360 for _, m := range maps { 361 for k, v := range m { 362 target[toKey(k)] = v 363 } 364 } 365 } 366 367 func symlinkJoin(filename, sym, parent string) (string, error) { 368 dir := path.Dir(filename) 369 dest := path.Join(dir, sym) 370 prefix := path.Clean(parent) + "/" 371 if strings.HasPrefix(dest, prefix) || prefix == "./" { 372 return dest, nil 373 } 374 return "", fmt.Errorf("symlink tries to access file '%s' outside of '%s'", strings.ReplaceAll(dest, "'", "''"), strings.ReplaceAll(parent, "'", "''")) 375 }