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