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