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

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"embed"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  
    17  	"github.com/kballard/go-shellquote"
    18  
    19  	"github.com/nektos/act/pkg/common"
    20  	"github.com/nektos/act/pkg/container"
    21  	"github.com/nektos/act/pkg/model"
    22  )
    23  
    24  type actionStep interface {
    25  	step
    26  
    27  	getActionModel() *model.Action
    28  	getCompositeRunContext(context.Context) *RunContext
    29  	getCompositeSteps() *compositeSteps
    30  }
    31  
    32  type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
    33  
    34  type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
    35  
    36  type fileWriter func(filename string, data []byte, perm fs.FileMode) error
    37  
    38  type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor
    39  
    40  //go:embed res/trampoline.js
    41  var trampoline embed.FS
    42  
    43  func readActionImpl(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
    44  	logger := common.Logger(ctx)
    45  	allErrors := []error{}
    46  	addError := func(fileName string, err error) {
    47  		if err != nil {
    48  			allErrors = append(allErrors, fmt.Errorf("failed to read '%s' from action '%s' with path '%s' of step: %w", fileName, step.String(), actionPath, err))
    49  		} else {
    50  			// One successful read, clear error state
    51  			allErrors = nil
    52  		}
    53  	}
    54  	reader, closer, err := readFile("action.yml")
    55  	addError("action.yml", err)
    56  	if os.IsNotExist(err) {
    57  		reader, closer, err = readFile("action.yaml")
    58  		addError("action.yaml", err)
    59  		if os.IsNotExist(err) {
    60  			_, closer, err := readFile("Dockerfile")
    61  			addError("Dockerfile", err)
    62  			if err == nil {
    63  				closer.Close()
    64  				action := &model.Action{
    65  					Name: "(Synthetic)",
    66  					Runs: model.ActionRuns{
    67  						Using: "docker",
    68  						Image: "Dockerfile",
    69  					},
    70  				}
    71  				logger.Debugf("Using synthetic action %v for Dockerfile", action)
    72  				return action, nil
    73  			}
    74  			if step.With != nil {
    75  				if val, ok := step.With["args"]; ok {
    76  					var b []byte
    77  					if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
    78  						return nil, err
    79  					}
    80  					err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0o400)
    81  					if err2 != nil {
    82  						return nil, err2
    83  					}
    84  					action := &model.Action{
    85  						Name: "(Synthetic)",
    86  						Inputs: map[string]model.Input{
    87  							"cwd": {
    88  								Description: "(Actual working directory)",
    89  								Required:    false,
    90  								Default:     filepath.Join(actionDir, actionPath),
    91  							},
    92  							"command": {
    93  								Description: "(Actual program)",
    94  								Required:    false,
    95  								Default:     val,
    96  							},
    97  						},
    98  						Runs: model.ActionRuns{
    99  							Using: "node12",
   100  							Main:  "trampoline.js",
   101  						},
   102  					}
   103  					logger.Debugf("Using synthetic action %v", action)
   104  					return action, nil
   105  				}
   106  			}
   107  		}
   108  	}
   109  	if allErrors != nil {
   110  		return nil, errors.Join(allErrors...)
   111  	}
   112  	defer closer.Close()
   113  
   114  	action, err := model.ReadAction(reader)
   115  	logger.Debugf("Read action %v from '%s'", action, "Unknown")
   116  	return action, err
   117  }
   118  
   119  func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir string, actionPath string, containerActionDir string) error {
   120  	logger := common.Logger(ctx)
   121  	rc := step.getRunContext()
   122  	stepModel := step.getStepModel()
   123  
   124  	if stepModel.Type() != model.StepTypeUsesActionRemote {
   125  		return nil
   126  	}
   127  
   128  	var containerActionDirCopy string
   129  	containerActionDirCopy = strings.TrimSuffix(containerActionDir, actionPath)
   130  	logger.Debug(containerActionDirCopy)
   131  
   132  	if !strings.HasSuffix(containerActionDirCopy, `/`) {
   133  		containerActionDirCopy += `/`
   134  	}
   135  
   136  	if rc.Config != nil && rc.Config.ActionCache != nil {
   137  		raction := step.(*stepActionRemote)
   138  		ta, err := rc.Config.ActionCache.GetTarArchive(ctx, raction.cacheDir, raction.resolvedSha, "")
   139  		if err != nil {
   140  			return err
   141  		}
   142  		defer ta.Close()
   143  		return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
   144  	}
   145  
   146  	if err := removeGitIgnore(ctx, actionDir); err != nil {
   147  		return err
   148  	}
   149  
   150  	return rc.JobContainer.CopyDir(containerActionDirCopy, actionDir+"/", rc.Config.UseGitIgnore)(ctx)
   151  }
   152  
   153  func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor {
   154  	rc := step.getRunContext()
   155  	stepModel := step.getStepModel()
   156  
   157  	return func(ctx context.Context) error {
   158  		logger := common.Logger(ctx)
   159  		actionPath := ""
   160  		if remoteAction != nil && remoteAction.Path != "" {
   161  			actionPath = remoteAction.Path
   162  		}
   163  
   164  		action := step.getActionModel()
   165  		logger.Debugf("About to run action %v", action)
   166  
   167  		err := setupActionEnv(ctx, step, remoteAction)
   168  		if err != nil {
   169  			return err
   170  		}
   171  
   172  		actionLocation := path.Join(actionDir, actionPath)
   173  		actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
   174  
   175  		logger.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", stepModel.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir)
   176  
   177  		x := action.Runs.Using
   178  		switch {
   179  		case x.IsNode():
   180  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   181  				return err
   182  			}
   183  			containerArgs := []string{rc.GetNodeToolFullPath(ctx), path.Join(containerActionDir, action.Runs.Main)}
   184  			logger.Debugf("executing remote job container: %s", containerArgs)
   185  
   186  			rc.ApplyExtraPath(ctx, step.getEnv())
   187  
   188  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   189  		case x.IsDocker():
   190  			if remoteAction == nil {
   191  				actionDir = ""
   192  				actionPath = containerActionDir
   193  			}
   194  			return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "entrypoint")
   195  		case x.IsComposite():
   196  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   197  				return err
   198  			}
   199  
   200  			return execAsComposite(step)(ctx)
   201  		default:
   202  			return fmt.Errorf("The runs.using key must be one of: %v, got %s", []string{
   203  				model.ActionRunsUsingDocker,
   204  				model.ActionRunsUsingNode12,
   205  				model.ActionRunsUsingNode16,
   206  				model.ActionRunsUsingNode20,
   207  				model.ActionRunsUsingNode24,
   208  				model.ActionRunsUsingComposite,
   209  			}, action.Runs.Using)
   210  		}
   211  	}
   212  }
   213  
   214  func setupActionEnv(ctx context.Context, step actionStep, _ *remoteAction) error {
   215  	rc := step.getRunContext()
   216  
   217  	// A few fields in the environment (e.g. GITHUB_ACTION_REPOSITORY)
   218  	// are dependent on the action. That means we can complete the
   219  	// setup only after resolving the whole action model and cloning
   220  	// the action
   221  	rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
   222  	populateEnvsFromSavedState(step.getEnv(), step, rc)
   223  	populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
   224  
   225  	return nil
   226  }
   227  
   228  // https://github.com/nektos/act/issues/228#issuecomment-629709055
   229  // files in .gitignore are not copied in a Docker container
   230  // this causes issues with actions that ignore other important resources
   231  // such as `node_modules` for example
   232  func removeGitIgnore(ctx context.Context, directory string) error {
   233  	gitIgnorePath := path.Join(directory, ".gitignore")
   234  	if _, err := os.Stat(gitIgnorePath); err == nil {
   235  		// .gitignore exists
   236  		common.Logger(ctx).Debugf("Removing %s before docker cp", gitIgnorePath)
   237  		err := os.Remove(gitIgnorePath)
   238  		if err != nil {
   239  			return err
   240  		}
   241  	}
   242  	return nil
   243  }
   244  
   245  // TODO: break out parts of function to reduce complexicity
   246  //
   247  //nolint:gocyclo
   248  func execAsDocker(ctx context.Context, step actionStep, actionName, basedir, subpath string, localAction bool, entrypointType string) error {
   249  	logger := common.Logger(ctx)
   250  	rc := step.getRunContext()
   251  	action := step.getActionModel()
   252  
   253  	var prepImage common.Executor
   254  	var image string
   255  	forcePull := false
   256  	if strings.HasPrefix(action.Runs.Image, "docker://") {
   257  		image = strings.TrimPrefix(action.Runs.Image, "docker://")
   258  		// Apply forcePull only for prebuild docker images
   259  		forcePull = rc.Config.ForcePull
   260  	} else {
   261  		// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
   262  		image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
   263  		image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
   264  		image = strings.ToLower(image)
   265  		contextDir, fileName := path.Split(path.Join(subpath, action.Runs.Image))
   266  
   267  		anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
   268  		if err != nil {
   269  			return err
   270  		}
   271  
   272  		correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
   273  		if err != nil {
   274  			return err
   275  		}
   276  
   277  		if anyArchExists && !correctArchExists {
   278  			wasRemoved, err := container.RemoveImage(ctx, image, true, true)
   279  			if err != nil {
   280  				return err
   281  			}
   282  			if !wasRemoved {
   283  				return fmt.Errorf("failed to remove image '%s'", image)
   284  			}
   285  		}
   286  
   287  		if !correctArchExists || rc.Config.ForceRebuild {
   288  			logger.Debugf("image '%s' for architecture '%s' will be built from context '%s", image, rc.Config.ContainerArchitecture, contextDir)
   289  			var buildContext io.ReadCloser
   290  			if localAction {
   291  				buildContext, err = rc.JobContainer.GetContainerArchive(ctx, contextDir+"/.")
   292  				if err != nil {
   293  					return err
   294  				}
   295  				defer buildContext.Close()
   296  			} else if rc.Config.ActionCache != nil {
   297  				rstep := step.(*stepActionRemote)
   298  				buildContext, err = rc.Config.ActionCache.GetTarArchive(ctx, rstep.cacheDir, rstep.resolvedSha, contextDir)
   299  				if err != nil {
   300  					return err
   301  				}
   302  				defer buildContext.Close()
   303  			}
   304  			prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
   305  				ContextDir:   filepath.Join(basedir, contextDir),
   306  				Dockerfile:   fileName,
   307  				ImageTag:     image,
   308  				BuildContext: buildContext,
   309  				Platform:     rc.Config.ContainerArchitecture,
   310  			})
   311  		} else {
   312  			logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
   313  		}
   314  	}
   315  	eval := rc.NewStepExpressionEvaluator(ctx, step)
   316  	cmd, err := shellquote.Split(eval.Interpolate(ctx, step.getStepModel().With["args"]))
   317  	if err != nil {
   318  		return err
   319  	}
   320  	if len(cmd) == 0 {
   321  		cmd = action.Runs.Args
   322  		evalDockerArgs(ctx, step, action, &cmd)
   323  	}
   324  
   325  	entrypoint := strings.Fields(eval.Interpolate(ctx, step.getStepModel().With[entrypointType]))
   326  	if len(entrypoint) == 0 {
   327  		if entrypointType == "pre-entrypoint" && action.Runs.PreEntrypoint != "" {
   328  			entrypoint, err = shellquote.Split(action.Runs.PreEntrypoint)
   329  			if err != nil {
   330  				return err
   331  			}
   332  		} else if entrypointType == "entrypoint" && action.Runs.Entrypoint != "" {
   333  			entrypoint, err = shellquote.Split(action.Runs.Entrypoint)
   334  			if err != nil {
   335  				return err
   336  			}
   337  		} else if entrypointType == "post-entrypoint" && action.Runs.PostEntrypoint != "" {
   338  			entrypoint, err = shellquote.Split(action.Runs.PostEntrypoint)
   339  			if err != nil {
   340  				return err
   341  			}
   342  		} else {
   343  			entrypoint = nil
   344  		}
   345  	}
   346  	stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
   347  	return common.NewPipelineExecutor(
   348  		prepImage,
   349  		stepContainer.Pull(forcePull),
   350  		stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
   351  		stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
   352  		stepContainer.Start(true),
   353  	).Finally(
   354  		stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
   355  	).Finally(stepContainer.Close())(ctx)
   356  }
   357  
   358  func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[]string) {
   359  	rc := step.getRunContext()
   360  	stepModel := step.getStepModel()
   361  
   362  	inputs := make(map[string]string)
   363  	eval := rc.NewExpressionEvaluator(ctx)
   364  	// Set Defaults
   365  	for k, input := range action.Inputs {
   366  		inputs[k] = eval.Interpolate(ctx, input.Default)
   367  	}
   368  	if stepModel.With != nil {
   369  		for k, v := range stepModel.With {
   370  			inputs[k] = eval.Interpolate(ctx, v)
   371  		}
   372  	}
   373  	mergeIntoMap(step, step.getEnv(), inputs)
   374  
   375  	stepEE := rc.NewStepExpressionEvaluator(ctx, step)
   376  	for i, v := range *cmd {
   377  		(*cmd)[i] = stepEE.Interpolate(ctx, v)
   378  	}
   379  	mergeIntoMap(step, step.getEnv(), action.Runs.Env)
   380  
   381  	ee := rc.NewStepExpressionEvaluator(ctx, step)
   382  	for k, v := range *step.getEnv() {
   383  		(*step.getEnv())[k] = ee.Interpolate(ctx, v)
   384  	}
   385  }
   386  
   387  func newStepContainer(ctx context.Context, step step, image string, cmd []string, entrypoint []string) container.Container {
   388  	rc := step.getRunContext()
   389  	stepModel := step.getStepModel()
   390  	rawLogger := common.Logger(ctx).WithField("raw_output", true)
   391  	logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
   392  		if rc.Config.LogOutput {
   393  			rawLogger.Infof("%s", s)
   394  		} else {
   395  			rawLogger.Debugf("%s", s)
   396  		}
   397  		return true
   398  	})
   399  	envList := make([]string, 0)
   400  	for k, v := range *step.getEnv() {
   401  		envList = append(envList, fmt.Sprintf("%s=%s", k, v))
   402  	}
   403  
   404  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
   405  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
   406  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
   407  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
   408  
   409  	binds, mounts := rc.GetBindsAndMounts()
   410  	networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
   411  	var workdir string
   412  	if rc.IsHostEnv(ctx) {
   413  		networkMode = "default"
   414  		ext := container.LinuxContainerEnvironmentExtensions{}
   415  		workdir = ext.ToContainerPath(rc.Config.Workdir)
   416  	} else {
   417  		workdir = rc.JobContainer.ToContainerPath(rc.Config.Workdir)
   418  	}
   419  	stepContainer := container.NewContainer(&container.NewContainerInput{
   420  		Cmd:         cmd,
   421  		Entrypoint:  entrypoint,
   422  		WorkingDir:  workdir,
   423  		Image:       image,
   424  		Username:    rc.Config.Secrets["DOCKER_USERNAME"],
   425  		Password:    rc.Config.Secrets["DOCKER_PASSWORD"],
   426  		Name:        createContainerName(rc.jobContainerName(), stepModel.ID),
   427  		Env:         envList,
   428  		Mounts:      mounts,
   429  		NetworkMode: networkMode,
   430  		Binds:       binds,
   431  		Stdout:      logWriter,
   432  		Stderr:      logWriter,
   433  		Privileged:  rc.Config.Privileged,
   434  		UsernsMode:  rc.Config.UsernsMode,
   435  		Platform:    rc.Config.ContainerArchitecture,
   436  		Options:     rc.Config.ContainerOptions,
   437  	})
   438  	return stepContainer
   439  }
   440  
   441  func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
   442  	state, ok := rc.IntraActionState[step.getStepModel().ID]
   443  	if ok {
   444  		for name, value := range state {
   445  			envName := fmt.Sprintf("STATE_%s", name)
   446  			(*env)[envName] = value
   447  		}
   448  	}
   449  }
   450  
   451  func populateEnvsFromInput(ctx context.Context, env *map[string]string, action *model.Action, rc *RunContext) {
   452  	eval := rc.NewExpressionEvaluator(ctx)
   453  	for inputID, input := range action.Inputs {
   454  		envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
   455  		envKey = fmt.Sprintf("INPUT_%s", envKey)
   456  		if _, ok := (*env)[envKey]; !ok {
   457  			(*env)[envKey] = eval.Interpolate(ctx, input.Default)
   458  		}
   459  	}
   460  }
   461  
   462  func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) {
   463  	actionName := ""
   464  	containerActionDir := "."
   465  	if step.Type() != model.StepTypeUsesActionRemote {
   466  		actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
   467  		containerActionDir = rc.JobContainer.ToContainerPath(rc.Config.Workdir) + "/" + actionName
   468  		actionName = "./" + actionName
   469  	} else if step.Type() == model.StepTypeUsesActionRemote {
   470  		actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
   471  		containerActionDir = rc.JobContainer.GetActPath() + "/actions/" + actionName
   472  	}
   473  
   474  	if actionName == "" {
   475  		actionName = filepath.Base(actionDir)
   476  		if runtime.GOOS == "windows" {
   477  			actionName = strings.ReplaceAll(actionName, "\\", "/")
   478  		}
   479  	}
   480  	return actionName, containerActionDir
   481  }
   482  
   483  func getOsSafeRelativePath(s, prefix string) string {
   484  	actionName := strings.TrimPrefix(s, prefix)
   485  	if runtime.GOOS == "windows" {
   486  		actionName = strings.ReplaceAll(actionName, "\\", "/")
   487  	}
   488  	actionName = strings.TrimPrefix(actionName, "/")
   489  
   490  	return actionName
   491  }
   492  
   493  func shouldRunPreStep(step actionStep) common.Conditional {
   494  	return func(ctx context.Context) bool {
   495  		log := common.Logger(ctx)
   496  
   497  		if step.getActionModel() == nil {
   498  			log.Debugf("skip pre step for '%s': no action model available", step.getStepModel())
   499  			return false
   500  		}
   501  
   502  		return true
   503  	}
   504  }
   505  
   506  func hasPreStep(step actionStep) common.Conditional {
   507  	return func(_ context.Context) bool {
   508  		action := step.getActionModel()
   509  		return action.Runs.Using.IsComposite() ||
   510  			(action.Runs.Using.IsNode() &&
   511  				action.Runs.Pre != "") ||
   512  			(action.Runs.Using.IsDocker() &&
   513  				action.Runs.PreEntrypoint != "")
   514  	}
   515  }
   516  
   517  func runPreStep(step actionStep) common.Executor {
   518  	return func(ctx context.Context) error {
   519  		logger := common.Logger(ctx)
   520  		logger.Debugf("run pre step for '%s'", step.getStepModel())
   521  
   522  		rc := step.getRunContext()
   523  		stepModel := step.getStepModel()
   524  		action := step.getActionModel()
   525  
   526  		// defaults in pre steps were missing, however provided inputs are available
   527  		populateEnvsFromInput(ctx, step.getEnv(), action, rc)
   528  
   529  		// todo: refactor into step
   530  		var actionDir string
   531  		var actionPath string
   532  		var remoteAction *stepActionRemote
   533  		if remote, ok := step.(*stepActionRemote); ok {
   534  			actionPath = newRemoteAction(stepModel.Uses).Path
   535  			actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
   536  			remoteAction = remote
   537  		} else {
   538  			actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
   539  			actionPath = ""
   540  		}
   541  
   542  		actionLocation := ""
   543  		if actionPath != "" {
   544  			actionLocation = path.Join(actionDir, actionPath)
   545  		} else {
   546  			actionLocation = actionDir
   547  		}
   548  
   549  		actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
   550  
   551  		x := action.Runs.Using
   552  		switch {
   553  		case x.IsNode():
   554  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   555  				return err
   556  			}
   557  
   558  			containerArgs := []string{rc.GetNodeToolFullPath(ctx), path.Join(containerActionDir, action.Runs.Pre)}
   559  			logger.Debugf("executing remote job container: %s", containerArgs)
   560  
   561  			rc.ApplyExtraPath(ctx, step.getEnv())
   562  
   563  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   564  
   565  		case x.IsDocker():
   566  			if remoteAction == nil {
   567  				actionDir = ""
   568  				actionPath = containerActionDir
   569  			}
   570  			return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "pre-entrypoint")
   571  
   572  		case x.IsComposite():
   573  			if step.getCompositeSteps() == nil {
   574  				step.getCompositeRunContext(ctx)
   575  			}
   576  
   577  			if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
   578  				return steps.pre(ctx)
   579  			}
   580  			return fmt.Errorf("missing steps in composite action")
   581  
   582  		default:
   583  			return nil
   584  		}
   585  	}
   586  }
   587  
   588  func shouldRunPostStep(step actionStep) common.Conditional {
   589  	return func(ctx context.Context) bool {
   590  		log := common.Logger(ctx)
   591  		stepResults := step.getRunContext().getStepsContext()
   592  		stepResult := stepResults[step.getStepModel().ID]
   593  
   594  		if stepResult == nil {
   595  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; step was not executed", step.getStepModel())
   596  			return false
   597  		}
   598  
   599  		if stepResult.Conclusion == model.StepStatusSkipped {
   600  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; main step was skipped", step.getStepModel())
   601  			return false
   602  		}
   603  
   604  		if step.getActionModel() == nil {
   605  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s': no action model available", step.getStepModel())
   606  			return false
   607  		}
   608  
   609  		return true
   610  	}
   611  }
   612  
   613  func hasPostStep(step actionStep) common.Conditional {
   614  	return func(_ context.Context) bool {
   615  		action := step.getActionModel()
   616  		return action.Runs.Using.IsComposite() ||
   617  			(action.Runs.Using.IsNode() &&
   618  				action.Runs.Post != "") ||
   619  			(action.Runs.Using.IsDocker() &&
   620  				action.Runs.PostEntrypoint != "")
   621  	}
   622  }
   623  
   624  func runPostStep(step actionStep) common.Executor {
   625  	return func(ctx context.Context) error {
   626  		logger := common.Logger(ctx)
   627  		logger.Debugf("run post step for '%s'", step.getStepModel())
   628  
   629  		rc := step.getRunContext()
   630  		stepModel := step.getStepModel()
   631  		action := step.getActionModel()
   632  
   633  		// todo: refactor into step
   634  		var actionDir string
   635  		var actionPath string
   636  		var remoteAction *stepActionRemote
   637  		if remote, ok := step.(*stepActionRemote); ok {
   638  			actionPath = newRemoteAction(stepModel.Uses).Path
   639  			actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
   640  			remoteAction = remote
   641  		} else {
   642  			actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
   643  			actionPath = ""
   644  		}
   645  
   646  		actionLocation := ""
   647  		if actionPath != "" {
   648  			actionLocation = path.Join(actionDir, actionPath)
   649  		} else {
   650  			actionLocation = actionDir
   651  		}
   652  
   653  		actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
   654  
   655  		x := action.Runs.Using
   656  		switch {
   657  		case x.IsNode():
   658  			populateEnvsFromSavedState(step.getEnv(), step, rc)
   659  			populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
   660  
   661  			containerArgs := []string{rc.GetNodeToolFullPath(ctx), path.Join(containerActionDir, action.Runs.Post)}
   662  			logger.Debugf("executing remote job container: %s", containerArgs)
   663  
   664  			rc.ApplyExtraPath(ctx, step.getEnv())
   665  
   666  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   667  
   668  		case x.IsDocker():
   669  			if remoteAction == nil {
   670  				actionDir = ""
   671  				actionPath = containerActionDir
   672  			}
   673  			return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "post-entrypoint")
   674  
   675  		case x.IsComposite():
   676  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   677  				return err
   678  			}
   679  
   680  			if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
   681  				return steps.post(ctx)
   682  			}
   683  			return fmt.Errorf("missing steps in composite action")
   684  
   685  		default:
   686  			return nil
   687  		}
   688  	}
   689  }