github.com/nektos/act@v0.2.83/pkg/runner/action_composite.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/nektos/act/pkg/common"
    10  	"github.com/nektos/act/pkg/model"
    11  )
    12  
    13  func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string {
    14  	env := make(map[string]string)
    15  	stepEnv := *step.getEnv()
    16  	for k, v := range stepEnv {
    17  		// do not set current inputs into composite action
    18  		// the required inputs are added in the second loop
    19  		if !strings.HasPrefix(k, "INPUT_") {
    20  			env[k] = v
    21  		}
    22  	}
    23  
    24  	ee := parent.NewStepExpressionEvaluator(ctx, step)
    25  
    26  	for inputID, input := range step.getActionModel().Inputs {
    27  		envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
    28  		envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey))
    29  
    30  		// lookup if key is defined in the step but the already
    31  		// evaluated value from the environment
    32  		_, defined := step.getStepModel().With[inputID]
    33  		if value, ok := stepEnv[envKey]; defined && ok {
    34  			env[envKey] = value
    35  		} else {
    36  			// defaults could contain expressions
    37  			env[envKey] = ee.Interpolate(ctx, input.Default)
    38  		}
    39  	}
    40  	gh := step.getGithubContext(ctx)
    41  	env["GITHUB_ACTION_REPOSITORY"] = gh.ActionRepository
    42  	env["GITHUB_ACTION_REF"] = gh.ActionRef
    43  
    44  	return env
    45  }
    46  
    47  func newCompositeRunContext(ctx context.Context, parent *RunContext, step actionStep, actionPath string) *RunContext {
    48  	env := evaluateCompositeInputAndEnv(ctx, parent, step)
    49  
    50  	// run with the global config but without secrets
    51  	configCopy := *(parent.Config)
    52  	configCopy.Secrets = nil
    53  
    54  	// create a run context for the composite action to run in
    55  	compositerc := &RunContext{
    56  		Name:    parent.Name,
    57  		JobName: parent.JobName,
    58  		Run: &model.Run{
    59  			JobID: parent.Run.JobID,
    60  			Workflow: &model.Workflow{
    61  				Name: parent.Run.Workflow.Name,
    62  				Jobs: map[string]*model.Job{
    63  					parent.Run.JobID: {},
    64  				},
    65  			},
    66  		},
    67  		Config:           &configCopy,
    68  		StepResults:      map[string]*model.StepResult{},
    69  		JobContainer:     parent.JobContainer,
    70  		ActionPath:       actionPath,
    71  		Env:              env,
    72  		GlobalEnv:        parent.GlobalEnv,
    73  		Masks:            parent.Masks,
    74  		ExtraPath:        parent.ExtraPath,
    75  		Parent:           parent,
    76  		EventJSON:        parent.EventJSON,
    77  		nodeToolFullPath: parent.nodeToolFullPath,
    78  	}
    79  	compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
    80  
    81  	return compositerc
    82  }
    83  
    84  func execAsComposite(step actionStep) common.Executor {
    85  	rc := step.getRunContext()
    86  	action := step.getActionModel()
    87  
    88  	return func(ctx context.Context) error {
    89  		compositeRC := step.getCompositeRunContext(ctx)
    90  
    91  		steps := step.getCompositeSteps()
    92  
    93  		if steps == nil || steps.main == nil {
    94  			return fmt.Errorf("missing steps in composite action")
    95  		}
    96  
    97  		ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
    98  
    99  		err := steps.main(ctx)
   100  
   101  		// Map outputs from composite RunContext to job RunContext
   102  		eval := compositeRC.NewExpressionEvaluator(ctx)
   103  		for outputName, output := range action.Outputs {
   104  			rc.setOutput(ctx, map[string]string{
   105  				"name": outputName,
   106  			}, eval.Interpolate(ctx, output.Value))
   107  		}
   108  
   109  		rc.Masks = append(rc.Masks, compositeRC.Masks...)
   110  		rc.ExtraPath = compositeRC.ExtraPath
   111  		// compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
   112  		mergeIntoMap := mergeIntoMapCaseSensitive
   113  		if rc.JobContainer.IsEnvironmentCaseInsensitive() {
   114  			mergeIntoMap = mergeIntoMapCaseInsensitive
   115  		}
   116  		if rc.GlobalEnv == nil {
   117  			rc.GlobalEnv = map[string]string{}
   118  		}
   119  		mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv)
   120  		mergeIntoMap(rc.Env, compositeRC.GlobalEnv)
   121  
   122  		return err
   123  	}
   124  }
   125  
   126  type compositeSteps struct {
   127  	pre  common.Executor
   128  	main common.Executor
   129  	post common.Executor
   130  }
   131  
   132  // Executor returns a pipeline executor for all the steps in the job
   133  func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
   134  	steps := make([]common.Executor, 0)
   135  	preSteps := make([]common.Executor, 0)
   136  	var postExecutor common.Executor
   137  
   138  	sf := &stepFactoryImpl{}
   139  
   140  	for i, step := range action.Runs.Steps {
   141  		if step.ID == "" {
   142  			step.ID = fmt.Sprintf("%d", i)
   143  		}
   144  
   145  		// create a copy of the step, since this composite action could
   146  		// run multiple times and we might modify the instance
   147  		stepcopy := step
   148  
   149  		step, err := sf.newStep(&stepcopy, rc)
   150  		if err != nil {
   151  			return &compositeSteps{
   152  				main: common.NewErrorExecutor(err),
   153  			}
   154  		}
   155  
   156  		stepID := step.getStepModel().ID
   157  		stepPre := rc.newCompositeCommandExecutor(step.pre())
   158  		preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID))
   159  
   160  		steps = append(steps, func(ctx context.Context) error {
   161  			ctx = WithCompositeStepLogger(ctx, stepID)
   162  			logger := common.Logger(ctx)
   163  			err := rc.newCompositeCommandExecutor(step.main())(ctx)
   164  
   165  			if err != nil {
   166  				logger.Errorf("%v", err)
   167  				common.SetJobError(ctx, err)
   168  			} else if ctx.Err() != nil {
   169  				logger.Errorf("%v", ctx.Err())
   170  				common.SetJobError(ctx, ctx.Err())
   171  			}
   172  			return nil
   173  		})
   174  
   175  		// run the post executor in reverse order
   176  		if postExecutor != nil {
   177  			stepPost := rc.newCompositeCommandExecutor(step.post())
   178  			postExecutor = newCompositeStepLogExecutor(stepPost.Finally(postExecutor), stepID)
   179  		} else {
   180  			stepPost := rc.newCompositeCommandExecutor(step.post())
   181  			postExecutor = newCompositeStepLogExecutor(stepPost, stepID)
   182  		}
   183  	}
   184  
   185  	steps = append(steps, common.JobError)
   186  	return &compositeSteps{
   187  		pre: func(ctx context.Context) error {
   188  			return common.NewPipelineExecutor(preSteps...)(common.WithJobErrorContainer(ctx))
   189  		},
   190  		main: func(ctx context.Context) error {
   191  			return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
   192  		},
   193  		post: postExecutor,
   194  	}
   195  }
   196  
   197  func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor {
   198  	return func(ctx context.Context) error {
   199  		ctx = WithCompositeLogger(ctx, &rc.Masks)
   200  
   201  		// We need to inject a composite RunContext related command
   202  		// handler into the current running job container
   203  		// We need this, to support scoping commands to the composite action
   204  		// executing.
   205  		rawLogger := common.Logger(ctx).WithField("raw_output", true)
   206  		logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
   207  			if rc.Config.LogOutput {
   208  				rawLogger.Infof("%s", s)
   209  			} else {
   210  				rawLogger.Debugf("%s", s)
   211  			}
   212  			return true
   213  		})
   214  
   215  		oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
   216  		defer rc.JobContainer.ReplaceLogWriter(oldout, olderr)
   217  
   218  		return executor(ctx)
   219  	}
   220  }
   221  
   222  func newCompositeStepLogExecutor(runStep common.Executor, stepID string) common.Executor {
   223  	return func(ctx context.Context) error {
   224  		ctx = WithCompositeStepLogger(ctx, stepID)
   225  		logger := common.Logger(ctx)
   226  		err := runStep(ctx)
   227  		if err != nil {
   228  			logger.Errorf("%v", err)
   229  			common.SetJobError(ctx, err)
   230  		} else if ctx.Err() != nil {
   231  			logger.Errorf("%v", ctx.Err())
   232  			common.SetJobError(ctx, ctx.Err())
   233  		}
   234  		return nil
   235  	}
   236  }