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  }