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  }