github.com/nektos/act@v0.2.63/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  		switch action.Runs.Using {
   178  		case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16, model.ActionRunsUsingNode20:
   179  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   180  				return err
   181  			}
   182  			containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
   183  			logger.Debugf("executing remote job container: %s", containerArgs)
   184  
   185  			rc.ApplyExtraPath(ctx, step.getEnv())
   186  
   187  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   188  		case model.ActionRunsUsingDocker:
   189  			location := actionLocation
   190  			if remoteAction == nil {
   191  				location = containerActionDir
   192  			}
   193  			return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
   194  		case model.ActionRunsUsingComposite:
   195  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   196  				return err
   197  			}
   198  
   199  			return execAsComposite(step)(ctx)
   200  		default:
   201  			return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
   202  				model.ActionRunsUsingDocker,
   203  				model.ActionRunsUsingNode12,
   204  				model.ActionRunsUsingNode16,
   205  				model.ActionRunsUsingNode20,
   206  				model.ActionRunsUsingComposite,
   207  			}, action.Runs.Using))
   208  		}
   209  	}
   210  }
   211  
   212  func setupActionEnv(ctx context.Context, step actionStep, _ *remoteAction) error {
   213  	rc := step.getRunContext()
   214  
   215  	// A few fields in the environment (e.g. GITHUB_ACTION_REPOSITORY)
   216  	// are dependent on the action. That means we can complete the
   217  	// setup only after resolving the whole action model and cloning
   218  	// the action
   219  	rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
   220  	populateEnvsFromSavedState(step.getEnv(), step, rc)
   221  	populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
   222  
   223  	return nil
   224  }
   225  
   226  // https://github.com/nektos/act/issues/228#issuecomment-629709055
   227  // files in .gitignore are not copied in a Docker container
   228  // this causes issues with actions that ignore other important resources
   229  // such as `node_modules` for example
   230  func removeGitIgnore(ctx context.Context, directory string) error {
   231  	gitIgnorePath := path.Join(directory, ".gitignore")
   232  	if _, err := os.Stat(gitIgnorePath); err == nil {
   233  		// .gitignore exists
   234  		common.Logger(ctx).Debugf("Removing %s before docker cp", gitIgnorePath)
   235  		err := os.Remove(gitIgnorePath)
   236  		if err != nil {
   237  			return err
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  // TODO: break out parts of function to reduce complexicity
   244  //
   245  //nolint:gocyclo
   246  func execAsDocker(ctx context.Context, step actionStep, actionName string, basedir string, localAction bool) error {
   247  	logger := common.Logger(ctx)
   248  	rc := step.getRunContext()
   249  	action := step.getActionModel()
   250  
   251  	var prepImage common.Executor
   252  	var image string
   253  	forcePull := false
   254  	if strings.HasPrefix(action.Runs.Image, "docker://") {
   255  		image = strings.TrimPrefix(action.Runs.Image, "docker://")
   256  		// Apply forcePull only for prebuild docker images
   257  		forcePull = rc.Config.ForcePull
   258  	} else {
   259  		// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
   260  		image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
   261  		image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
   262  		image = strings.ToLower(image)
   263  		contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
   264  
   265  		anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
   266  		if err != nil {
   267  			return err
   268  		}
   269  
   270  		correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
   271  		if err != nil {
   272  			return err
   273  		}
   274  
   275  		if anyArchExists && !correctArchExists {
   276  			wasRemoved, err := container.RemoveImage(ctx, image, true, true)
   277  			if err != nil {
   278  				return err
   279  			}
   280  			if !wasRemoved {
   281  				return fmt.Errorf("failed to remove image '%s'", image)
   282  			}
   283  		}
   284  
   285  		if !correctArchExists || rc.Config.ForceRebuild {
   286  			logger.Debugf("image '%s' for architecture '%s' will be built from context '%s", image, rc.Config.ContainerArchitecture, contextDir)
   287  			var buildContext io.ReadCloser
   288  			if localAction {
   289  				buildContext, err = rc.JobContainer.GetContainerArchive(ctx, contextDir+"/.")
   290  				if err != nil {
   291  					return err
   292  				}
   293  				defer buildContext.Close()
   294  			} else if rc.Config.ActionCache != nil {
   295  				rstep := step.(*stepActionRemote)
   296  				buildContext, err = rc.Config.ActionCache.GetTarArchive(ctx, rstep.cacheDir, rstep.resolvedSha, contextDir)
   297  				if err != nil {
   298  					return err
   299  				}
   300  				defer buildContext.Close()
   301  			}
   302  			prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
   303  				ContextDir:   contextDir,
   304  				Dockerfile:   fileName,
   305  				ImageTag:     image,
   306  				BuildContext: buildContext,
   307  				Platform:     rc.Config.ContainerArchitecture,
   308  			})
   309  		} else {
   310  			logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
   311  		}
   312  	}
   313  	eval := rc.NewStepExpressionEvaluator(ctx, step)
   314  	cmd, err := shellquote.Split(eval.Interpolate(ctx, step.getStepModel().With["args"]))
   315  	if err != nil {
   316  		return err
   317  	}
   318  	if len(cmd) == 0 {
   319  		cmd = action.Runs.Args
   320  		evalDockerArgs(ctx, step, action, &cmd)
   321  	}
   322  	entrypoint := strings.Fields(eval.Interpolate(ctx, step.getStepModel().With["entrypoint"]))
   323  	if len(entrypoint) == 0 {
   324  		if action.Runs.Entrypoint != "" {
   325  			entrypoint, err = shellquote.Split(action.Runs.Entrypoint)
   326  			if err != nil {
   327  				return err
   328  			}
   329  		} else {
   330  			entrypoint = nil
   331  		}
   332  	}
   333  	stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
   334  	return common.NewPipelineExecutor(
   335  		prepImage,
   336  		stepContainer.Pull(forcePull),
   337  		stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
   338  		stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
   339  		stepContainer.Start(true),
   340  	).Finally(
   341  		stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
   342  	).Finally(stepContainer.Close())(ctx)
   343  }
   344  
   345  func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[]string) {
   346  	rc := step.getRunContext()
   347  	stepModel := step.getStepModel()
   348  
   349  	inputs := make(map[string]string)
   350  	eval := rc.NewExpressionEvaluator(ctx)
   351  	// Set Defaults
   352  	for k, input := range action.Inputs {
   353  		inputs[k] = eval.Interpolate(ctx, input.Default)
   354  	}
   355  	if stepModel.With != nil {
   356  		for k, v := range stepModel.With {
   357  			inputs[k] = eval.Interpolate(ctx, v)
   358  		}
   359  	}
   360  	mergeIntoMap(step, step.getEnv(), inputs)
   361  
   362  	stepEE := rc.NewStepExpressionEvaluator(ctx, step)
   363  	for i, v := range *cmd {
   364  		(*cmd)[i] = stepEE.Interpolate(ctx, v)
   365  	}
   366  	mergeIntoMap(step, step.getEnv(), action.Runs.Env)
   367  
   368  	ee := rc.NewStepExpressionEvaluator(ctx, step)
   369  	for k, v := range *step.getEnv() {
   370  		(*step.getEnv())[k] = ee.Interpolate(ctx, v)
   371  	}
   372  }
   373  
   374  func newStepContainer(ctx context.Context, step step, image string, cmd []string, entrypoint []string) container.Container {
   375  	rc := step.getRunContext()
   376  	stepModel := step.getStepModel()
   377  	rawLogger := common.Logger(ctx).WithField("raw_output", true)
   378  	logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
   379  		if rc.Config.LogOutput {
   380  			rawLogger.Infof("%s", s)
   381  		} else {
   382  			rawLogger.Debugf("%s", s)
   383  		}
   384  		return true
   385  	})
   386  	envList := make([]string, 0)
   387  	for k, v := range *step.getEnv() {
   388  		envList = append(envList, fmt.Sprintf("%s=%s", k, v))
   389  	}
   390  
   391  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
   392  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
   393  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
   394  	envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
   395  
   396  	binds, mounts := rc.GetBindsAndMounts()
   397  	networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
   398  	if rc.IsHostEnv(ctx) {
   399  		networkMode = "default"
   400  	}
   401  	stepContainer := container.NewContainer(&container.NewContainerInput{
   402  		Cmd:         cmd,
   403  		Entrypoint:  entrypoint,
   404  		WorkingDir:  rc.JobContainer.ToContainerPath(rc.Config.Workdir),
   405  		Image:       image,
   406  		Username:    rc.Config.Secrets["DOCKER_USERNAME"],
   407  		Password:    rc.Config.Secrets["DOCKER_PASSWORD"],
   408  		Name:        createContainerName(rc.jobContainerName(), stepModel.ID),
   409  		Env:         envList,
   410  		Mounts:      mounts,
   411  		NetworkMode: networkMode,
   412  		Binds:       binds,
   413  		Stdout:      logWriter,
   414  		Stderr:      logWriter,
   415  		Privileged:  rc.Config.Privileged,
   416  		UsernsMode:  rc.Config.UsernsMode,
   417  		Platform:    rc.Config.ContainerArchitecture,
   418  		Options:     rc.Config.ContainerOptions,
   419  	})
   420  	return stepContainer
   421  }
   422  
   423  func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
   424  	state, ok := rc.IntraActionState[step.getStepModel().ID]
   425  	if ok {
   426  		for name, value := range state {
   427  			envName := fmt.Sprintf("STATE_%s", name)
   428  			(*env)[envName] = value
   429  		}
   430  	}
   431  }
   432  
   433  func populateEnvsFromInput(ctx context.Context, env *map[string]string, action *model.Action, rc *RunContext) {
   434  	eval := rc.NewExpressionEvaluator(ctx)
   435  	for inputID, input := range action.Inputs {
   436  		envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
   437  		envKey = fmt.Sprintf("INPUT_%s", envKey)
   438  		if _, ok := (*env)[envKey]; !ok {
   439  			(*env)[envKey] = eval.Interpolate(ctx, input.Default)
   440  		}
   441  	}
   442  }
   443  
   444  func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) {
   445  	actionName := ""
   446  	containerActionDir := "."
   447  	if step.Type() != model.StepTypeUsesActionRemote {
   448  		actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
   449  		containerActionDir = rc.JobContainer.ToContainerPath(rc.Config.Workdir) + "/" + actionName
   450  		actionName = "./" + actionName
   451  	} else if step.Type() == model.StepTypeUsesActionRemote {
   452  		actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
   453  		containerActionDir = rc.JobContainer.GetActPath() + "/actions/" + actionName
   454  	}
   455  
   456  	if actionName == "" {
   457  		actionName = filepath.Base(actionDir)
   458  		if runtime.GOOS == "windows" {
   459  			actionName = strings.ReplaceAll(actionName, "\\", "/")
   460  		}
   461  	}
   462  	return actionName, containerActionDir
   463  }
   464  
   465  func getOsSafeRelativePath(s, prefix string) string {
   466  	actionName := strings.TrimPrefix(s, prefix)
   467  	if runtime.GOOS == "windows" {
   468  		actionName = strings.ReplaceAll(actionName, "\\", "/")
   469  	}
   470  	actionName = strings.TrimPrefix(actionName, "/")
   471  
   472  	return actionName
   473  }
   474  
   475  func shouldRunPreStep(step actionStep) common.Conditional {
   476  	return func(ctx context.Context) bool {
   477  		log := common.Logger(ctx)
   478  
   479  		if step.getActionModel() == nil {
   480  			log.Debugf("skip pre step for '%s': no action model available", step.getStepModel())
   481  			return false
   482  		}
   483  
   484  		return true
   485  	}
   486  }
   487  
   488  func hasPreStep(step actionStep) common.Conditional {
   489  	return func(ctx context.Context) bool {
   490  		action := step.getActionModel()
   491  		return action.Runs.Using == model.ActionRunsUsingComposite ||
   492  			((action.Runs.Using == model.ActionRunsUsingNode12 ||
   493  				action.Runs.Using == model.ActionRunsUsingNode16 ||
   494  				action.Runs.Using == model.ActionRunsUsingNode20) &&
   495  				action.Runs.Pre != "")
   496  	}
   497  }
   498  
   499  func runPreStep(step actionStep) common.Executor {
   500  	return func(ctx context.Context) error {
   501  		logger := common.Logger(ctx)
   502  		logger.Debugf("run pre step for '%s'", step.getStepModel())
   503  
   504  		rc := step.getRunContext()
   505  		stepModel := step.getStepModel()
   506  		action := step.getActionModel()
   507  
   508  		switch action.Runs.Using {
   509  		case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16, model.ActionRunsUsingNode20:
   510  			// defaults in pre steps were missing, however provided inputs are available
   511  			populateEnvsFromInput(ctx, step.getEnv(), action, rc)
   512  			// todo: refactor into step
   513  			var actionDir string
   514  			var actionPath string
   515  			if _, ok := step.(*stepActionRemote); ok {
   516  				actionPath = newRemoteAction(stepModel.Uses).Path
   517  				actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
   518  			} else {
   519  				actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
   520  				actionPath = ""
   521  			}
   522  
   523  			actionLocation := ""
   524  			if actionPath != "" {
   525  				actionLocation = path.Join(actionDir, actionPath)
   526  			} else {
   527  				actionLocation = actionDir
   528  			}
   529  
   530  			_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
   531  
   532  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   533  				return err
   534  			}
   535  
   536  			containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
   537  			logger.Debugf("executing remote job container: %s", containerArgs)
   538  
   539  			rc.ApplyExtraPath(ctx, step.getEnv())
   540  
   541  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   542  
   543  		case model.ActionRunsUsingComposite:
   544  			if step.getCompositeSteps() == nil {
   545  				step.getCompositeRunContext(ctx)
   546  			}
   547  
   548  			if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
   549  				return steps.pre(ctx)
   550  			}
   551  			return fmt.Errorf("missing steps in composite action")
   552  
   553  		default:
   554  			return nil
   555  		}
   556  	}
   557  }
   558  
   559  func shouldRunPostStep(step actionStep) common.Conditional {
   560  	return func(ctx context.Context) bool {
   561  		log := common.Logger(ctx)
   562  		stepResults := step.getRunContext().getStepsContext()
   563  		stepResult := stepResults[step.getStepModel().ID]
   564  
   565  		if stepResult == nil {
   566  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; step was not executed", step.getStepModel())
   567  			return false
   568  		}
   569  
   570  		if stepResult.Conclusion == model.StepStatusSkipped {
   571  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; main step was skipped", step.getStepModel())
   572  			return false
   573  		}
   574  
   575  		if step.getActionModel() == nil {
   576  			log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s': no action model available", step.getStepModel())
   577  			return false
   578  		}
   579  
   580  		return true
   581  	}
   582  }
   583  
   584  func hasPostStep(step actionStep) common.Conditional {
   585  	return func(ctx context.Context) bool {
   586  		action := step.getActionModel()
   587  		return action.Runs.Using == model.ActionRunsUsingComposite ||
   588  			((action.Runs.Using == model.ActionRunsUsingNode12 ||
   589  				action.Runs.Using == model.ActionRunsUsingNode16 ||
   590  				action.Runs.Using == model.ActionRunsUsingNode20) &&
   591  				action.Runs.Post != "")
   592  	}
   593  }
   594  
   595  func runPostStep(step actionStep) common.Executor {
   596  	return func(ctx context.Context) error {
   597  		logger := common.Logger(ctx)
   598  		logger.Debugf("run post step for '%s'", step.getStepModel())
   599  
   600  		rc := step.getRunContext()
   601  		stepModel := step.getStepModel()
   602  		action := step.getActionModel()
   603  
   604  		// todo: refactor into step
   605  		var actionDir string
   606  		var actionPath string
   607  		if _, ok := step.(*stepActionRemote); ok {
   608  			actionPath = newRemoteAction(stepModel.Uses).Path
   609  			actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
   610  		} else {
   611  			actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
   612  			actionPath = ""
   613  		}
   614  
   615  		actionLocation := ""
   616  		if actionPath != "" {
   617  			actionLocation = path.Join(actionDir, actionPath)
   618  		} else {
   619  			actionLocation = actionDir
   620  		}
   621  
   622  		_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
   623  
   624  		switch action.Runs.Using {
   625  		case model.ActionRunsUsingNode12, model.ActionRunsUsingNode16, model.ActionRunsUsingNode20:
   626  
   627  			populateEnvsFromSavedState(step.getEnv(), step, rc)
   628  
   629  			containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
   630  			logger.Debugf("executing remote job container: %s", containerArgs)
   631  
   632  			rc.ApplyExtraPath(ctx, step.getEnv())
   633  
   634  			return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
   635  
   636  		case model.ActionRunsUsingComposite:
   637  			if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
   638  				return err
   639  			}
   640  
   641  			if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
   642  				return steps.post(ctx)
   643  			}
   644  			return fmt.Errorf("missing steps in composite action")
   645  
   646  		default:
   647  			return nil
   648  		}
   649  	}
   650  }