github.com/nektos/act@v0.2.63/pkg/runner/run_context.go (about)

     1  package runner
     2  
     3  import (
     4  	"archive/tar"
     5  	"bufio"
     6  	"context"
     7  	"crypto/rand"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime"
    18  	"strconv"
    19  	"strings"
    20  
    21  	"github.com/docker/go-connections/nat"
    22  	"github.com/nektos/act/pkg/common"
    23  	"github.com/nektos/act/pkg/container"
    24  	"github.com/nektos/act/pkg/exprparser"
    25  	"github.com/nektos/act/pkg/model"
    26  	"github.com/opencontainers/selinux/go-selinux"
    27  )
    28  
    29  // RunContext contains info about current job
    30  type RunContext struct {
    31  	Name                string
    32  	Config              *Config
    33  	Matrix              map[string]interface{}
    34  	Run                 *model.Run
    35  	EventJSON           string
    36  	Env                 map[string]string
    37  	GlobalEnv           map[string]string // to pass env changes of GITHUB_ENV and set-env correctly, due to dirty Env field
    38  	ExtraPath           []string
    39  	CurrentStep         string
    40  	StepResults         map[string]*model.StepResult
    41  	IntraActionState    map[string]map[string]string
    42  	ExprEval            ExpressionEvaluator
    43  	JobContainer        container.ExecutionsEnvironment
    44  	ServiceContainers   []container.ExecutionsEnvironment
    45  	OutputMappings      map[MappableOutput]MappableOutput
    46  	JobName             string
    47  	ActionPath          string
    48  	Parent              *RunContext
    49  	Masks               []string
    50  	cleanUpJobContainer common.Executor
    51  	caller              *caller // job calling this RunContext (reusable workflows)
    52  }
    53  
    54  func (rc *RunContext) AddMask(mask string) {
    55  	rc.Masks = append(rc.Masks, mask)
    56  }
    57  
    58  type MappableOutput struct {
    59  	StepID     string
    60  	OutputName string
    61  }
    62  
    63  func (rc *RunContext) String() string {
    64  	name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
    65  	if rc.caller != nil {
    66  		// prefix the reusable workflow with the caller job
    67  		// this is required to create unique container names
    68  		name = fmt.Sprintf("%s/%s", rc.caller.runContext.Name, name)
    69  	}
    70  	return name
    71  }
    72  
    73  // GetEnv returns the env for the context
    74  func (rc *RunContext) GetEnv() map[string]string {
    75  	if rc.Env == nil {
    76  		rc.Env = map[string]string{}
    77  		if rc.Run != nil && rc.Run.Workflow != nil && rc.Config != nil {
    78  			job := rc.Run.Job()
    79  			if job != nil {
    80  				rc.Env = mergeMaps(rc.Run.Workflow.Env, job.Environment(), rc.Config.Env)
    81  			}
    82  		}
    83  	}
    84  	rc.Env["ACT"] = "true"
    85  	return rc.Env
    86  }
    87  
    88  func (rc *RunContext) jobContainerName() string {
    89  	return createContainerName("act", rc.String())
    90  }
    91  
    92  // networkName return the name of the network which will be created by `act` automatically for job,
    93  // only create network if using a service container
    94  func (rc *RunContext) networkName() (string, bool) {
    95  	if len(rc.Run.Job().Services) > 0 {
    96  		return fmt.Sprintf("%s-%s-network", rc.jobContainerName(), rc.Run.JobID), true
    97  	}
    98  	if rc.Config.ContainerNetworkMode == "" {
    99  		return "host", false
   100  	}
   101  	return string(rc.Config.ContainerNetworkMode), false
   102  }
   103  
   104  func getDockerDaemonSocketMountPath(daemonPath string) string {
   105  	if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 {
   106  		scheme := daemonPath[:protoIndex]
   107  		if strings.EqualFold(scheme, "npipe") {
   108  			// linux container mount on windows, use the default socket path of the VM / wsl2
   109  			return "/var/run/docker.sock"
   110  		} else if strings.EqualFold(scheme, "unix") {
   111  			return daemonPath[protoIndex+3:]
   112  		} else if strings.IndexFunc(scheme, func(r rune) bool {
   113  			return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
   114  		}) == -1 {
   115  			// unknown protocol use default
   116  			return "/var/run/docker.sock"
   117  		}
   118  	}
   119  	return daemonPath
   120  }
   121  
   122  // Returns the binds and mounts for the container, resolving paths as appropriate
   123  func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
   124  	name := rc.jobContainerName()
   125  
   126  	if rc.Config.ContainerDaemonSocket == "" {
   127  		rc.Config.ContainerDaemonSocket = "/var/run/docker.sock"
   128  	}
   129  
   130  	binds := []string{}
   131  	if rc.Config.ContainerDaemonSocket != "-" {
   132  		daemonPath := getDockerDaemonSocketMountPath(rc.Config.ContainerDaemonSocket)
   133  		binds = append(binds, fmt.Sprintf("%s:%s", daemonPath, "/var/run/docker.sock"))
   134  	}
   135  
   136  	ext := container.LinuxContainerEnvironmentExtensions{}
   137  
   138  	mounts := map[string]string{
   139  		"act-toolcache": "/opt/hostedtoolcache",
   140  		name + "-env":   ext.GetActPath(),
   141  	}
   142  
   143  	if job := rc.Run.Job(); job != nil {
   144  		if container := job.Container(); container != nil {
   145  			for _, v := range container.Volumes {
   146  				if !strings.Contains(v, ":") || filepath.IsAbs(v) {
   147  					// Bind anonymous volume or host file.
   148  					binds = append(binds, v)
   149  				} else {
   150  					// Mount existing volume.
   151  					paths := strings.SplitN(v, ":", 2)
   152  					mounts[paths[0]] = paths[1]
   153  				}
   154  			}
   155  		}
   156  	}
   157  
   158  	if rc.Config.BindWorkdir {
   159  		bindModifiers := ""
   160  		if runtime.GOOS == "darwin" {
   161  			bindModifiers = ":delegated"
   162  		}
   163  		if selinux.GetEnabled() {
   164  			bindModifiers = ":z"
   165  		}
   166  		binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, ext.ToContainerPath(rc.Config.Workdir), bindModifiers))
   167  	} else {
   168  		mounts[name] = ext.ToContainerPath(rc.Config.Workdir)
   169  	}
   170  
   171  	return binds, mounts
   172  }
   173  
   174  func (rc *RunContext) startHostEnvironment() common.Executor {
   175  	return func(ctx context.Context) error {
   176  		logger := common.Logger(ctx)
   177  		rawLogger := logger.WithField("raw_output", true)
   178  		logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
   179  			if rc.Config.LogOutput {
   180  				rawLogger.Infof("%s", s)
   181  			} else {
   182  				rawLogger.Debugf("%s", s)
   183  			}
   184  			return true
   185  		})
   186  		cacheDir := rc.ActionCacheDir()
   187  		randBytes := make([]byte, 8)
   188  		_, _ = rand.Read(randBytes)
   189  		miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes))
   190  		actPath := filepath.Join(miscpath, "act")
   191  		if err := os.MkdirAll(actPath, 0o777); err != nil {
   192  			return err
   193  		}
   194  		path := filepath.Join(miscpath, "hostexecutor")
   195  		if err := os.MkdirAll(path, 0o777); err != nil {
   196  			return err
   197  		}
   198  		runnerTmp := filepath.Join(miscpath, "tmp")
   199  		if err := os.MkdirAll(runnerTmp, 0o777); err != nil {
   200  			return err
   201  		}
   202  		toolCache := filepath.Join(cacheDir, "tool_cache")
   203  		rc.JobContainer = &container.HostEnvironment{
   204  			Path:      path,
   205  			TmpDir:    runnerTmp,
   206  			ToolCache: toolCache,
   207  			Workdir:   rc.Config.Workdir,
   208  			ActPath:   actPath,
   209  			CleanUp: func() {
   210  				os.RemoveAll(miscpath)
   211  			},
   212  			StdOut: logWriter,
   213  		}
   214  		rc.cleanUpJobContainer = rc.JobContainer.Remove()
   215  		for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
   216  			if v, ok := v.(string); ok {
   217  				rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
   218  			}
   219  		}
   220  		for _, env := range os.Environ() {
   221  			if k, v, ok := strings.Cut(env, "="); ok {
   222  				// don't override
   223  				if _, ok := rc.Env[k]; !ok {
   224  					rc.Env[k] = v
   225  				}
   226  			}
   227  		}
   228  
   229  		return common.NewPipelineExecutor(
   230  			rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
   231  				Name: "workflow/event.json",
   232  				Mode: 0o644,
   233  				Body: rc.EventJSON,
   234  			}, &container.FileEntry{
   235  				Name: "workflow/envs.txt",
   236  				Mode: 0o666,
   237  				Body: "",
   238  			}),
   239  		)(ctx)
   240  	}
   241  }
   242  
   243  //nolint:gocyclo
   244  func (rc *RunContext) startJobContainer() common.Executor {
   245  	return func(ctx context.Context) error {
   246  		logger := common.Logger(ctx)
   247  		image := rc.platformImage(ctx)
   248  		rawLogger := logger.WithField("raw_output", true)
   249  		logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
   250  			if rc.Config.LogOutput {
   251  				rawLogger.Infof("%s", s)
   252  			} else {
   253  				rawLogger.Debugf("%s", s)
   254  			}
   255  			return true
   256  		})
   257  
   258  		username, password, err := rc.handleCredentials(ctx)
   259  		if err != nil {
   260  			return fmt.Errorf("failed to handle credentials: %s", err)
   261  		}
   262  
   263  		logger.Infof("\U0001f680  Start image=%s", image)
   264  		name := rc.jobContainerName()
   265  
   266  		envList := make([]string, 0)
   267  
   268  		envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
   269  		envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
   270  		envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
   271  		envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
   272  		envList = append(envList, fmt.Sprintf("%s=%s", "LANG", "C.UTF-8")) // Use same locale as GitHub Actions
   273  
   274  		ext := container.LinuxContainerEnvironmentExtensions{}
   275  		binds, mounts := rc.GetBindsAndMounts()
   276  
   277  		// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
   278  		// if using service containers, will create a new network for the containers.
   279  		// and it will be removed after at last.
   280  		networkName, createAndDeleteNetwork := rc.networkName()
   281  
   282  		// add service containers
   283  		for serviceID, spec := range rc.Run.Job().Services {
   284  			// interpolate env
   285  			interpolatedEnvs := make(map[string]string, len(spec.Env))
   286  			for k, v := range spec.Env {
   287  				interpolatedEnvs[k] = rc.ExprEval.Interpolate(ctx, v)
   288  			}
   289  			envs := make([]string, 0, len(interpolatedEnvs))
   290  			for k, v := range interpolatedEnvs {
   291  				envs = append(envs, fmt.Sprintf("%s=%s", k, v))
   292  			}
   293  			username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials)
   294  			if err != nil {
   295  				return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
   296  			}
   297  
   298  			interpolatedVolumes := make([]string, 0, len(spec.Volumes))
   299  			for _, volume := range spec.Volumes {
   300  				interpolatedVolumes = append(interpolatedVolumes, rc.ExprEval.Interpolate(ctx, volume))
   301  			}
   302  			serviceBinds, serviceMounts := rc.GetServiceBindsAndMounts(interpolatedVolumes)
   303  
   304  			interpolatedPorts := make([]string, 0, len(spec.Ports))
   305  			for _, port := range spec.Ports {
   306  				interpolatedPorts = append(interpolatedPorts, rc.ExprEval.Interpolate(ctx, port))
   307  			}
   308  			exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
   309  			if err != nil {
   310  				return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
   311  			}
   312  
   313  			serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
   314  			c := container.NewContainer(&container.NewContainerInput{
   315  				Name:           serviceContainerName,
   316  				WorkingDir:     ext.ToContainerPath(rc.Config.Workdir),
   317  				Image:          rc.ExprEval.Interpolate(ctx, spec.Image),
   318  				Username:       username,
   319  				Password:       password,
   320  				Env:            envs,
   321  				Mounts:         serviceMounts,
   322  				Binds:          serviceBinds,
   323  				Stdout:         logWriter,
   324  				Stderr:         logWriter,
   325  				Privileged:     rc.Config.Privileged,
   326  				UsernsMode:     rc.Config.UsernsMode,
   327  				Platform:       rc.Config.ContainerArchitecture,
   328  				Options:        rc.ExprEval.Interpolate(ctx, spec.Options),
   329  				NetworkMode:    networkName,
   330  				NetworkAliases: []string{serviceID},
   331  				ExposedPorts:   exposedPorts,
   332  				PortBindings:   portBindings,
   333  			})
   334  			rc.ServiceContainers = append(rc.ServiceContainers, c)
   335  		}
   336  
   337  		rc.cleanUpJobContainer = func(ctx context.Context) error {
   338  			reuseJobContainer := func(ctx context.Context) bool {
   339  				return rc.Config.ReuseContainers
   340  			}
   341  
   342  			if rc.JobContainer != nil {
   343  				return rc.JobContainer.Remove().IfNot(reuseJobContainer).
   344  					Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).IfNot(reuseJobContainer).
   345  					Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false)).IfNot(reuseJobContainer).
   346  					Then(func(ctx context.Context) error {
   347  						if len(rc.ServiceContainers) > 0 {
   348  							logger.Infof("Cleaning up services for job %s", rc.JobName)
   349  							if err := rc.stopServiceContainers()(ctx); err != nil {
   350  								logger.Errorf("Error while cleaning services: %v", err)
   351  							}
   352  							if createAndDeleteNetwork {
   353  								// clean network if it has been created by act
   354  								// if using service containers
   355  								// it means that the network to which containers are connecting is created by `act_runner`,
   356  								// so, we should remove the network at last.
   357  								logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
   358  								if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
   359  									logger.Errorf("Error while cleaning network: %v", err)
   360  								}
   361  							}
   362  						}
   363  						return nil
   364  					})(ctx)
   365  			}
   366  			return nil
   367  		}
   368  
   369  		jobContainerNetwork := rc.Config.ContainerNetworkMode.NetworkName()
   370  		if rc.containerImage(ctx) != "" {
   371  			jobContainerNetwork = networkName
   372  		} else if jobContainerNetwork == "" {
   373  			jobContainerNetwork = "host"
   374  		}
   375  
   376  		rc.JobContainer = container.NewContainer(&container.NewContainerInput{
   377  			Cmd:            nil,
   378  			Entrypoint:     []string{"tail", "-f", "/dev/null"},
   379  			WorkingDir:     ext.ToContainerPath(rc.Config.Workdir),
   380  			Image:          image,
   381  			Username:       username,
   382  			Password:       password,
   383  			Name:           name,
   384  			Env:            envList,
   385  			Mounts:         mounts,
   386  			NetworkMode:    jobContainerNetwork,
   387  			NetworkAliases: []string{rc.Name},
   388  			Binds:          binds,
   389  			Stdout:         logWriter,
   390  			Stderr:         logWriter,
   391  			Privileged:     rc.Config.Privileged,
   392  			UsernsMode:     rc.Config.UsernsMode,
   393  			Platform:       rc.Config.ContainerArchitecture,
   394  			Options:        rc.options(ctx),
   395  		})
   396  		if rc.JobContainer == nil {
   397  			return errors.New("Failed to create job container")
   398  		}
   399  
   400  		return common.NewPipelineExecutor(
   401  			rc.pullServicesImages(rc.Config.ForcePull),
   402  			rc.JobContainer.Pull(rc.Config.ForcePull),
   403  			rc.stopJobContainer(),
   404  			container.NewDockerNetworkCreateExecutor(networkName).IfBool(createAndDeleteNetwork),
   405  			rc.startServiceContainers(networkName),
   406  			rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
   407  			rc.JobContainer.Start(false),
   408  			rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
   409  				Name: "workflow/event.json",
   410  				Mode: 0o644,
   411  				Body: rc.EventJSON,
   412  			}, &container.FileEntry{
   413  				Name: "workflow/envs.txt",
   414  				Mode: 0o666,
   415  				Body: "",
   416  			}),
   417  		)(ctx)
   418  	}
   419  }
   420  
   421  func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor {
   422  	return func(ctx context.Context) error {
   423  		return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx)
   424  	}
   425  }
   426  
   427  func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) {
   428  	if rc.ExtraPath != nil && len(rc.ExtraPath) > 0 {
   429  		path := rc.JobContainer.GetPathVariableName()
   430  		if rc.JobContainer.IsEnvironmentCaseInsensitive() {
   431  			// On windows system Path and PATH could also be in the map
   432  			for k := range *env {
   433  				if strings.EqualFold(path, k) {
   434  					path = k
   435  					break
   436  				}
   437  			}
   438  		}
   439  		if (*env)[path] == "" {
   440  			cenv := map[string]string{}
   441  			var cpath string
   442  			if err := rc.JobContainer.UpdateFromImageEnv(&cenv)(ctx); err == nil {
   443  				if p, ok := cenv[path]; ok {
   444  					cpath = p
   445  				}
   446  			}
   447  			if len(cpath) == 0 {
   448  				cpath = rc.JobContainer.DefaultPathVariable()
   449  			}
   450  			(*env)[path] = cpath
   451  		}
   452  		(*env)[path] = rc.JobContainer.JoinPathVariable(append(rc.ExtraPath, (*env)[path])...)
   453  	}
   454  }
   455  
   456  func (rc *RunContext) UpdateExtraPath(ctx context.Context, githubEnvPath string) error {
   457  	if common.Dryrun(ctx) {
   458  		return nil
   459  	}
   460  	pathTar, err := rc.JobContainer.GetContainerArchive(ctx, githubEnvPath)
   461  	if err != nil {
   462  		return err
   463  	}
   464  	defer pathTar.Close()
   465  
   466  	reader := tar.NewReader(pathTar)
   467  	_, err = reader.Next()
   468  	if err != nil && err != io.EOF {
   469  		return err
   470  	}
   471  	s := bufio.NewScanner(reader)
   472  	for s.Scan() {
   473  		line := s.Text()
   474  		if len(line) > 0 {
   475  			rc.addPath(ctx, line)
   476  		}
   477  	}
   478  	return nil
   479  }
   480  
   481  // stopJobContainer removes the job container (if it exists) and its volume (if it exists)
   482  func (rc *RunContext) stopJobContainer() common.Executor {
   483  	return func(ctx context.Context) error {
   484  		if rc.cleanUpJobContainer != nil {
   485  			return rc.cleanUpJobContainer(ctx)
   486  		}
   487  		return nil
   488  	}
   489  }
   490  
   491  func (rc *RunContext) pullServicesImages(forcePull bool) common.Executor {
   492  	return func(ctx context.Context) error {
   493  		execs := []common.Executor{}
   494  		for _, c := range rc.ServiceContainers {
   495  			execs = append(execs, c.Pull(forcePull))
   496  		}
   497  		return common.NewParallelExecutor(len(execs), execs...)(ctx)
   498  	}
   499  }
   500  
   501  func (rc *RunContext) startServiceContainers(_ string) common.Executor {
   502  	return func(ctx context.Context) error {
   503  		execs := []common.Executor{}
   504  		for _, c := range rc.ServiceContainers {
   505  			execs = append(execs, common.NewPipelineExecutor(
   506  				c.Pull(false),
   507  				c.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
   508  				c.Start(false),
   509  			))
   510  		}
   511  		return common.NewParallelExecutor(len(execs), execs...)(ctx)
   512  	}
   513  }
   514  
   515  func (rc *RunContext) stopServiceContainers() common.Executor {
   516  	return func(ctx context.Context) error {
   517  		execs := []common.Executor{}
   518  		for _, c := range rc.ServiceContainers {
   519  			execs = append(execs, c.Remove().Finally(c.Close()))
   520  		}
   521  		return common.NewParallelExecutor(len(execs), execs...)(ctx)
   522  	}
   523  }
   524  
   525  // Prepare the mounts and binds for the worker
   526  
   527  // ActionCacheDir is for rc
   528  func (rc *RunContext) ActionCacheDir() string {
   529  	if rc.Config.ActionCacheDir != "" {
   530  		return rc.Config.ActionCacheDir
   531  	}
   532  	var xdgCache string
   533  	var ok bool
   534  	if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok || xdgCache == "" {
   535  		if home, err := os.UserHomeDir(); err == nil {
   536  			xdgCache = filepath.Join(home, ".cache")
   537  		} else if xdgCache, err = filepath.Abs("."); err != nil {
   538  			// It's almost impossible to get here, so the temp dir is a good fallback
   539  			xdgCache = os.TempDir()
   540  		}
   541  	}
   542  	return filepath.Join(xdgCache, "act")
   543  }
   544  
   545  // Interpolate outputs after a job is done
   546  func (rc *RunContext) interpolateOutputs() common.Executor {
   547  	return func(ctx context.Context) error {
   548  		ee := rc.NewExpressionEvaluator(ctx)
   549  		for k, v := range rc.Run.Job().Outputs {
   550  			interpolated := ee.Interpolate(ctx, v)
   551  			if v != interpolated {
   552  				rc.Run.Job().Outputs[k] = interpolated
   553  			}
   554  		}
   555  		return nil
   556  	}
   557  }
   558  
   559  func (rc *RunContext) startContainer() common.Executor {
   560  	return func(ctx context.Context) error {
   561  		if rc.IsHostEnv(ctx) {
   562  			return rc.startHostEnvironment()(ctx)
   563  		}
   564  		return rc.startJobContainer()(ctx)
   565  	}
   566  }
   567  
   568  func (rc *RunContext) IsHostEnv(ctx context.Context) bool {
   569  	platform := rc.runsOnImage(ctx)
   570  	image := rc.containerImage(ctx)
   571  	return image == "" && strings.EqualFold(platform, "-self-hosted")
   572  }
   573  
   574  func (rc *RunContext) stopContainer() common.Executor {
   575  	return rc.stopJobContainer()
   576  }
   577  
   578  func (rc *RunContext) closeContainer() common.Executor {
   579  	return func(ctx context.Context) error {
   580  		if rc.JobContainer != nil {
   581  			return rc.JobContainer.Close()(ctx)
   582  		}
   583  		return nil
   584  	}
   585  }
   586  
   587  func (rc *RunContext) matrix() map[string]interface{} {
   588  	return rc.Matrix
   589  }
   590  
   591  func (rc *RunContext) result(result string) {
   592  	rc.Run.Job().Result = result
   593  }
   594  
   595  func (rc *RunContext) steps() []*model.Step {
   596  	return rc.Run.Job().Steps
   597  }
   598  
   599  // Executor returns a pipeline executor for all the steps in the job
   600  func (rc *RunContext) Executor() (common.Executor, error) {
   601  	var executor common.Executor
   602  	var jobType, err = rc.Run.Job().Type()
   603  
   604  	switch jobType {
   605  	case model.JobTypeDefault:
   606  		executor = newJobExecutor(rc, &stepFactoryImpl{}, rc)
   607  	case model.JobTypeReusableWorkflowLocal:
   608  		executor = newLocalReusableWorkflowExecutor(rc)
   609  	case model.JobTypeReusableWorkflowRemote:
   610  		executor = newRemoteReusableWorkflowExecutor(rc)
   611  	case model.JobTypeInvalid:
   612  		return nil, err
   613  	}
   614  
   615  	return func(ctx context.Context) error {
   616  		res, err := rc.isEnabled(ctx)
   617  		if err != nil {
   618  			return err
   619  		}
   620  		if res {
   621  			return executor(ctx)
   622  		}
   623  		return nil
   624  	}, nil
   625  }
   626  
   627  func (rc *RunContext) containerImage(ctx context.Context) string {
   628  	job := rc.Run.Job()
   629  
   630  	c := job.Container()
   631  	if c != nil {
   632  		return rc.ExprEval.Interpolate(ctx, c.Image)
   633  	}
   634  
   635  	return ""
   636  }
   637  
   638  func (rc *RunContext) runsOnImage(ctx context.Context) string {
   639  	if rc.Run.Job().RunsOn() == nil {
   640  		common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String())
   641  	}
   642  
   643  	for _, platformName := range rc.runsOnPlatformNames(ctx) {
   644  		image := rc.Config.Platforms[strings.ToLower(platformName)]
   645  		if image != "" {
   646  			return image
   647  		}
   648  	}
   649  
   650  	return ""
   651  }
   652  
   653  func (rc *RunContext) runsOnPlatformNames(ctx context.Context) []string {
   654  	job := rc.Run.Job()
   655  
   656  	if job.RunsOn() == nil {
   657  		return []string{}
   658  	}
   659  
   660  	if err := rc.ExprEval.EvaluateYamlNode(ctx, &job.RawRunsOn); err != nil {
   661  		common.Logger(ctx).Errorf("Error while evaluating runs-on: %v", err)
   662  		return []string{}
   663  	}
   664  
   665  	return job.RunsOn()
   666  }
   667  
   668  func (rc *RunContext) platformImage(ctx context.Context) string {
   669  	if containerImage := rc.containerImage(ctx); containerImage != "" {
   670  		return containerImage
   671  	}
   672  
   673  	return rc.runsOnImage(ctx)
   674  }
   675  
   676  func (rc *RunContext) options(ctx context.Context) string {
   677  	job := rc.Run.Job()
   678  	c := job.Container()
   679  	if c != nil {
   680  		return rc.ExprEval.Interpolate(ctx, c.Options)
   681  	}
   682  
   683  	return rc.Config.ContainerOptions
   684  }
   685  
   686  func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
   687  	job := rc.Run.Job()
   688  	l := common.Logger(ctx)
   689  	runJob, runJobErr := EvalBool(ctx, rc.ExprEval, job.If.Value, exprparser.DefaultStatusCheckSuccess)
   690  	jobType, jobTypeErr := job.Type()
   691  
   692  	if runJobErr != nil {
   693  		return false, fmt.Errorf("  \u274C  Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr)
   694  	}
   695  
   696  	if jobType == model.JobTypeInvalid {
   697  		return false, jobTypeErr
   698  	}
   699  
   700  	if !runJob {
   701  		rc.result("skipped")
   702  		l.WithField("jobResult", "skipped").Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value)
   703  		return false, nil
   704  	}
   705  
   706  	if jobType != model.JobTypeDefault {
   707  		return true, nil
   708  	}
   709  
   710  	img := rc.platformImage(ctx)
   711  	if img == "" {
   712  		for _, platformName := range rc.runsOnPlatformNames(ctx) {
   713  			l.Infof("\U0001F6A7  Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
   714  		}
   715  		return false, nil
   716  	}
   717  	return true, nil
   718  }
   719  
   720  func mergeMaps(maps ...map[string]string) map[string]string {
   721  	rtnMap := make(map[string]string)
   722  	for _, m := range maps {
   723  		for k, v := range m {
   724  			rtnMap[k] = v
   725  		}
   726  	}
   727  	return rtnMap
   728  }
   729  
   730  func createContainerName(parts ...string) string {
   731  	name := strings.Join(parts, "-")
   732  	pattern := regexp.MustCompile("[^a-zA-Z0-9]")
   733  	name = pattern.ReplaceAllString(name, "-")
   734  	name = strings.ReplaceAll(name, "--", "-")
   735  	hash := sha256.Sum256([]byte(name))
   736  
   737  	// SHA256 is 64 hex characters. So trim name to 63 characters to make room for the hash and separator
   738  	trimmedName := strings.Trim(trimToLen(name, 63), "-")
   739  
   740  	return fmt.Sprintf("%s-%x", trimmedName, hash)
   741  }
   742  
   743  func trimToLen(s string, l int) string {
   744  	if l < 0 {
   745  		l = 0
   746  	}
   747  	if len(s) > l {
   748  		return s[:l]
   749  	}
   750  	return s
   751  }
   752  
   753  func (rc *RunContext) getJobContext() *model.JobContext {
   754  	jobStatus := "success"
   755  	for _, stepStatus := range rc.StepResults {
   756  		if stepStatus.Conclusion == model.StepStatusFailure {
   757  			jobStatus = "failure"
   758  			break
   759  		}
   760  	}
   761  	return &model.JobContext{
   762  		Status: jobStatus,
   763  	}
   764  }
   765  
   766  func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
   767  	return rc.StepResults
   768  }
   769  
   770  func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
   771  	logger := common.Logger(ctx)
   772  	ghc := &model.GithubContext{
   773  		Event:            make(map[string]interface{}),
   774  		Workflow:         rc.Run.Workflow.Name,
   775  		RunID:            rc.Config.Env["GITHUB_RUN_ID"],
   776  		RunNumber:        rc.Config.Env["GITHUB_RUN_NUMBER"],
   777  		Actor:            rc.Config.Actor,
   778  		EventName:        rc.Config.EventName,
   779  		Action:           rc.CurrentStep,
   780  		Token:            rc.Config.Token,
   781  		Job:              rc.Run.JobID,
   782  		ActionPath:       rc.ActionPath,
   783  		ActionRepository: rc.Env["GITHUB_ACTION_REPOSITORY"],
   784  		ActionRef:        rc.Env["GITHUB_ACTION_REF"],
   785  		RepositoryOwner:  rc.Config.Env["GITHUB_REPOSITORY_OWNER"],
   786  		RetentionDays:    rc.Config.Env["GITHUB_RETENTION_DAYS"],
   787  		RunnerPerflog:    rc.Config.Env["RUNNER_PERFLOG"],
   788  		RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"],
   789  		Repository:       rc.Config.Env["GITHUB_REPOSITORY"],
   790  		Ref:              rc.Config.Env["GITHUB_REF"],
   791  		Sha:              rc.Config.Env["SHA_REF"],
   792  		RefName:          rc.Config.Env["GITHUB_REF_NAME"],
   793  		RefType:          rc.Config.Env["GITHUB_REF_TYPE"],
   794  		BaseRef:          rc.Config.Env["GITHUB_BASE_REF"],
   795  		HeadRef:          rc.Config.Env["GITHUB_HEAD_REF"],
   796  		Workspace:        rc.Config.Env["GITHUB_WORKSPACE"],
   797  	}
   798  	if rc.JobContainer != nil {
   799  		ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json"
   800  		ghc.Workspace = rc.JobContainer.ToContainerPath(rc.Config.Workdir)
   801  	}
   802  
   803  	if ghc.RunID == "" {
   804  		ghc.RunID = "1"
   805  	}
   806  
   807  	if ghc.RunNumber == "" {
   808  		ghc.RunNumber = "1"
   809  	}
   810  
   811  	if ghc.RetentionDays == "" {
   812  		ghc.RetentionDays = "0"
   813  	}
   814  
   815  	if ghc.RunnerPerflog == "" {
   816  		ghc.RunnerPerflog = "/dev/null"
   817  	}
   818  
   819  	// Backwards compatibility for configs that require
   820  	// a default rather than being run as a cmd
   821  	if ghc.Actor == "" {
   822  		ghc.Actor = "nektos/act"
   823  	}
   824  
   825  	if rc.EventJSON != "" {
   826  		err := json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
   827  		if err != nil {
   828  			logger.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err)
   829  		}
   830  	}
   831  
   832  	ghc.SetBaseAndHeadRef()
   833  	repoPath := rc.Config.Workdir
   834  	ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath)
   835  	if ghc.Ref == "" {
   836  		ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath)
   837  	}
   838  	if ghc.Sha == "" {
   839  		ghc.SetSha(ctx, repoPath)
   840  	}
   841  
   842  	ghc.SetRefTypeAndName()
   843  
   844  	// defaults
   845  	ghc.ServerURL = "https://github.com"
   846  	ghc.APIURL = "https://api.github.com"
   847  	ghc.GraphQLURL = "https://api.github.com/graphql"
   848  	// per GHES
   849  	if rc.Config.GitHubInstance != "github.com" {
   850  		ghc.ServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
   851  		ghc.APIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
   852  		ghc.GraphQLURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
   853  	}
   854  	// allow to be overridden by user
   855  	if rc.Config.Env["GITHUB_SERVER_URL"] != "" {
   856  		ghc.ServerURL = rc.Config.Env["GITHUB_SERVER_URL"]
   857  	}
   858  	if rc.Config.Env["GITHUB_API_URL"] != "" {
   859  		ghc.APIURL = rc.Config.Env["GITHUB_API_URL"]
   860  	}
   861  	if rc.Config.Env["GITHUB_GRAPHQL_URL"] != "" {
   862  		ghc.GraphQLURL = rc.Config.Env["GITHUB_GRAPHQL_URL"]
   863  	}
   864  
   865  	return ghc
   866  }
   867  
   868  func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
   869  	if step.Type() == model.StepTypeInvalid {
   870  		// This will be errored out by the executor later, we need this here to avoid a null panic though
   871  		return false
   872  	}
   873  	if step.Type() != model.StepTypeUsesActionRemote {
   874  		return false
   875  	}
   876  	remoteAction := newRemoteAction(step.Uses)
   877  	if remoteAction == nil {
   878  		// IsCheckout() will nil panic if we dont bail out early
   879  		return false
   880  	}
   881  	if !remoteAction.IsCheckout() {
   882  		return false
   883  	}
   884  
   885  	if repository, ok := step.With["repository"]; ok && repository != ghc.Repository {
   886  		return false
   887  	}
   888  	if repository, ok := step.With["ref"]; ok && repository != ghc.Ref {
   889  		return false
   890  	}
   891  	return true
   892  }
   893  
   894  func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) {
   895  	var ok bool
   896  
   897  	if len(ks) == 0 { // degenerate input
   898  		return nil
   899  	}
   900  	if rval, ok = m[ks[0]]; !ok {
   901  		return nil
   902  	} else if len(ks) == 1 { // we've reached the final key
   903  		return rval
   904  	} else if m, ok = rval.(map[string]interface{}); !ok {
   905  		return nil
   906  	} else { // 1+ more keys
   907  		return nestedMapLookup(m, ks[1:]...)
   908  	}
   909  }
   910  
   911  func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string {
   912  	env["CI"] = "true"
   913  	env["GITHUB_WORKFLOW"] = github.Workflow
   914  	env["GITHUB_RUN_ID"] = github.RunID
   915  	env["GITHUB_RUN_NUMBER"] = github.RunNumber
   916  	env["GITHUB_ACTION"] = github.Action
   917  	env["GITHUB_ACTION_PATH"] = github.ActionPath
   918  	env["GITHUB_ACTION_REPOSITORY"] = github.ActionRepository
   919  	env["GITHUB_ACTION_REF"] = github.ActionRef
   920  	env["GITHUB_ACTIONS"] = "true"
   921  	env["GITHUB_ACTOR"] = github.Actor
   922  	env["GITHUB_REPOSITORY"] = github.Repository
   923  	env["GITHUB_EVENT_NAME"] = github.EventName
   924  	env["GITHUB_EVENT_PATH"] = github.EventPath
   925  	env["GITHUB_WORKSPACE"] = github.Workspace
   926  	env["GITHUB_SHA"] = github.Sha
   927  	env["GITHUB_REF"] = github.Ref
   928  	env["GITHUB_REF_NAME"] = github.RefName
   929  	env["GITHUB_REF_TYPE"] = github.RefType
   930  	env["GITHUB_JOB"] = github.Job
   931  	env["GITHUB_REPOSITORY_OWNER"] = github.RepositoryOwner
   932  	env["GITHUB_RETENTION_DAYS"] = github.RetentionDays
   933  	env["RUNNER_PERFLOG"] = github.RunnerPerflog
   934  	env["RUNNER_TRACKING_ID"] = github.RunnerTrackingID
   935  	env["GITHUB_BASE_REF"] = github.BaseRef
   936  	env["GITHUB_HEAD_REF"] = github.HeadRef
   937  	env["GITHUB_SERVER_URL"] = github.ServerURL
   938  	env["GITHUB_API_URL"] = github.APIURL
   939  	env["GITHUB_GRAPHQL_URL"] = github.GraphQLURL
   940  
   941  	if rc.Config.ArtifactServerPath != "" {
   942  		setActionRuntimeVars(rc, env)
   943  	}
   944  
   945  	for _, platformName := range rc.runsOnPlatformNames(ctx) {
   946  		if platformName != "" {
   947  			if platformName == "ubuntu-latest" {
   948  				// hardcode current ubuntu-latest since we have no way to check that 'on the fly'
   949  				env["ImageOS"] = "ubuntu20"
   950  			} else {
   951  				platformName = strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
   952  				env["ImageOS"] = platformName
   953  			}
   954  		}
   955  	}
   956  
   957  	return env
   958  }
   959  
   960  func setActionRuntimeVars(rc *RunContext, env map[string]string) {
   961  	actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL")
   962  	if actionsRuntimeURL == "" {
   963  		actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", rc.Config.ArtifactServerAddr, rc.Config.ArtifactServerPort)
   964  	}
   965  	env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL
   966  	env["ACTIONS_RESULTS_URL"] = actionsRuntimeURL
   967  
   968  	actionsRuntimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN")
   969  	if actionsRuntimeToken == "" {
   970  		runID := int64(1)
   971  		if rid, ok := rc.Config.Env["GITHUB_RUN_ID"]; ok {
   972  			runID, _ = strconv.ParseInt(rid, 10, 64)
   973  		}
   974  		actionsRuntimeToken, _ = common.CreateAuthorizationToken(runID, runID, runID)
   975  	}
   976  	env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
   977  }
   978  
   979  func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, error) {
   980  	// TODO: remove below 2 lines when we can release act with breaking changes
   981  	username := rc.Config.Secrets["DOCKER_USERNAME"]
   982  	password := rc.Config.Secrets["DOCKER_PASSWORD"]
   983  
   984  	container := rc.Run.Job().Container()
   985  	if container == nil || container.Credentials == nil {
   986  		return username, password, nil
   987  	}
   988  
   989  	if container.Credentials != nil && len(container.Credentials) != 2 {
   990  		err := fmt.Errorf("invalid property count for key 'credentials:'")
   991  		return "", "", err
   992  	}
   993  
   994  	ee := rc.NewExpressionEvaluator(ctx)
   995  	if username = ee.Interpolate(ctx, container.Credentials["username"]); username == "" {
   996  		err := fmt.Errorf("failed to interpolate container.credentials.username")
   997  		return "", "", err
   998  	}
   999  	if password = ee.Interpolate(ctx, container.Credentials["password"]); password == "" {
  1000  		err := fmt.Errorf("failed to interpolate container.credentials.password")
  1001  		return "", "", err
  1002  	}
  1003  
  1004  	if container.Credentials["username"] == "" || container.Credentials["password"] == "" {
  1005  		err := fmt.Errorf("container.credentials cannot be empty")
  1006  		return "", "", err
  1007  	}
  1008  
  1009  	return username, password, nil
  1010  }
  1011  
  1012  func (rc *RunContext) handleServiceCredentials(ctx context.Context, creds map[string]string) (username, password string, err error) {
  1013  	if creds == nil {
  1014  		return
  1015  	}
  1016  	if len(creds) != 2 {
  1017  		err = fmt.Errorf("invalid property count for key 'credentials:'")
  1018  		return
  1019  	}
  1020  
  1021  	ee := rc.NewExpressionEvaluator(ctx)
  1022  	if username = ee.Interpolate(ctx, creds["username"]); username == "" {
  1023  		err = fmt.Errorf("failed to interpolate credentials.username")
  1024  		return
  1025  	}
  1026  
  1027  	if password = ee.Interpolate(ctx, creds["password"]); password == "" {
  1028  		err = fmt.Errorf("failed to interpolate credentials.password")
  1029  		return
  1030  	}
  1031  
  1032  	return
  1033  }
  1034  
  1035  // GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appropriate
  1036  func (rc *RunContext) GetServiceBindsAndMounts(svcVolumes []string) ([]string, map[string]string) {
  1037  	if rc.Config.ContainerDaemonSocket == "" {
  1038  		rc.Config.ContainerDaemonSocket = "/var/run/docker.sock"
  1039  	}
  1040  	binds := []string{}
  1041  	if rc.Config.ContainerDaemonSocket != "-" {
  1042  		daemonPath := getDockerDaemonSocketMountPath(rc.Config.ContainerDaemonSocket)
  1043  		binds = append(binds, fmt.Sprintf("%s:%s", daemonPath, "/var/run/docker.sock"))
  1044  	}
  1045  
  1046  	mounts := map[string]string{}
  1047  
  1048  	for _, v := range svcVolumes {
  1049  		if !strings.Contains(v, ":") || filepath.IsAbs(v) {
  1050  			// Bind anonymous volume or host file.
  1051  			binds = append(binds, v)
  1052  		} else {
  1053  			// Mount existing volume.
  1054  			paths := strings.SplitN(v, ":", 2)
  1055  			mounts[paths[0]] = paths[1]
  1056  		}
  1057  	}
  1058  
  1059  	return binds, mounts
  1060  }