github.com/nektos/act@v0.2.63/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  	}
    78  	compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
    79  
    80  	return compositerc
    81  }
    82  
    83  func execAsComposite(step actionStep) common.Executor {
    84  	rc := step.getRunContext()
    85  	action := step.getActionModel()
    86  
    87  	return func(ctx context.Context) error {
    88  		compositeRC := step.getCompositeRunContext(ctx)
    89  
    90  		steps := step.getCompositeSteps()
    91  
    92  		if steps == nil || steps.main == nil {
    93  			return fmt.Errorf("missing steps in composite action")
    94  		}
    95  
    96  		ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
    97  
    98  		err := steps.main(ctx)
    99  
   100  		// Map outputs from composite RunContext to job RunContext
   101  		eval := compositeRC.NewExpressionEvaluator(ctx)
   102  		for outputName, output := range action.Outputs {
   103  			rc.setOutput(ctx, map[string]string{
   104  				"name": outputName,
   105  			}, eval.Interpolate(ctx, output.Value))
   106  		}
   107  
   108  		rc.Masks = append(rc.Masks, compositeRC.Masks...)
   109  		rc.ExtraPath = compositeRC.ExtraPath
   110  		// compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
   111  		mergeIntoMap := mergeIntoMapCaseSensitive
   112  		if rc.JobContainer.IsEnvironmentCaseInsensitive() {
   113  			mergeIntoMap = mergeIntoMapCaseInsensitive
   114  		}
   115  		if rc.GlobalEnv == nil {
   116  			rc.GlobalEnv = map[string]string{}
   117  		}
   118  		mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv)
   119  		mergeIntoMap(rc.Env, compositeRC.GlobalEnv)
   120  
   121  		return err
   122  	}
   123  }
   124  
   125  type compositeSteps struct {
   126  	pre  common.Executor
   127  	main common.Executor
   128  	post common.Executor
   129  }
   130  
   131  // Executor returns a pipeline executor for all the steps in the job
   132  func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
   133  	steps := make([]common.Executor, 0)
   134  	preSteps := make([]common.Executor, 0)
   135  	var postExecutor common.Executor
   136  
   137  	sf := &stepFactoryImpl{}
   138  
   139  	for i, step := range action.Runs.Steps {
   140  		if step.ID == "" {
   141  			step.ID = fmt.Sprintf("%d", i)
   142  		}
   143  
   144  		// create a copy of the step, since this composite action could
   145  		// run multiple times and we might modify the instance
   146  		stepcopy := step
   147  
   148  		step, err := sf.newStep(&stepcopy, rc)
   149  		if err != nil {
   150  			return &compositeSteps{
   151  				main: common.NewErrorExecutor(err),
   152  			}
   153  		}
   154  
   155  		stepID := step.getStepModel().ID
   156  		stepPre := rc.newCompositeCommandExecutor(step.pre())
   157  		preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID))
   158  
   159  		steps = append(steps, func(ctx context.Context) error {
   160  			ctx = WithCompositeStepLogger(ctx, stepID)
   161  			logger := common.Logger(ctx)
   162  			err := rc.newCompositeCommandExecutor(step.main())(ctx)
   163  
   164  			if err != nil {
   165  				logger.Errorf("%v", err)
   166  				common.SetJobError(ctx, err)
   167  			} else if ctx.Err() != nil {
   168  				logger.Errorf("%v", ctx.Err())
   169  				common.SetJobError(ctx, ctx.Err())
   170  			}
   171  			return nil
   172  		})
   173  
   174  		// run the post executor in reverse order
   175  		if postExecutor != nil {
   176  			stepPost := rc.newCompositeCommandExecutor(step.post())
   177  			postExecutor = newCompositeStepLogExecutor(stepPost.Finally(postExecutor), stepID)
   178  		} else {
   179  			stepPost := rc.newCompositeCommandExecutor(step.post())
   180  			postExecutor = newCompositeStepLogExecutor(stepPost, stepID)
   181  		}
   182  	}
   183  
   184  	steps = append(steps, common.JobError)
   185  	return &compositeSteps{
   186  		pre: func(ctx context.Context) error {
   187  			return common.NewPipelineExecutor(preSteps...)(common.WithJobErrorContainer(ctx))
   188  		},
   189  		main: func(ctx context.Context) error {
   190  			return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
   191  		},
   192  		post: postExecutor,
   193  	}
   194  }
   195  
   196  func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor {
   197  	return func(ctx context.Context) error {
   198  		ctx = WithCompositeLogger(ctx, &rc.Masks)
   199  
   200  		// We need to inject a composite RunContext related command
   201  		// handler into the current running job container
   202  		// We need this, to support scoping commands to the composite action
   203  		// executing.
   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  }
   220  
   221  func newCompositeStepLogExecutor(runStep common.Executor, stepID string) common.Executor {
   222  	return func(ctx context.Context) error {
   223  		ctx = WithCompositeStepLogger(ctx, stepID)
   224  		logger := common.Logger(ctx)
   225  		err := runStep(ctx)
   226  		if err != nil {
   227  			logger.Errorf("%v", err)
   228  			common.SetJobError(ctx, err)
   229  		} else if ctx.Err() != nil {
   230  			logger.Errorf("%v", ctx.Err())
   231  			common.SetJobError(ctx, ctx.Err())
   232  		}
   233  		return nil
   234  	}
   235  }