github.com/nektos/act@v0.2.83/cmd/root.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"runtime/debug"
    14  	"strings"
    15  
    16  	"github.com/AlecAivazis/survey/v2"
    17  	"github.com/adrg/xdg"
    18  	"github.com/andreaskoch/go-fswatch"
    19  	docker_container "github.com/docker/docker/api/types/container"
    20  	"github.com/joho/godotenv"
    21  	gitignore "github.com/sabhiram/go-gitignore"
    22  	log "github.com/sirupsen/logrus"
    23  	"github.com/spf13/cobra"
    24  	"github.com/spf13/cobra/doc"
    25  	"github.com/spf13/pflag"
    26  	"gopkg.in/yaml.v3"
    27  
    28  	"github.com/nektos/act/pkg/artifactcache"
    29  	"github.com/nektos/act/pkg/artifacts"
    30  	"github.com/nektos/act/pkg/common"
    31  	"github.com/nektos/act/pkg/container"
    32  	"github.com/nektos/act/pkg/gh"
    33  	"github.com/nektos/act/pkg/model"
    34  	"github.com/nektos/act/pkg/runner"
    35  )
    36  
    37  type Flag struct {
    38  	Name        string `json:"name"`
    39  	Default     string `json:"default"`
    40  	Type        string `json:"type"`
    41  	Description string `json:"description"`
    42  }
    43  
    44  var exitFunc = os.Exit
    45  
    46  // Execute is the entry point to running the CLI
    47  func Execute(ctx context.Context, version string) {
    48  	input := new(Input)
    49  	rootCmd := createRootCommand(ctx, input, version)
    50  
    51  	if err := rootCmd.Execute(); err != nil {
    52  		exitFunc(1)
    53  	}
    54  }
    55  
    56  func createRootCommand(ctx context.Context, input *Input, version string) *cobra.Command {
    57  	rootCmd := &cobra.Command{
    58  		Use:               "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
    59  		Short:             "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
    60  		Args:              cobra.MaximumNArgs(1),
    61  		RunE:              newRunCommand(ctx, input),
    62  		PersistentPreRun:  setup(input),
    63  		PersistentPostRun: cleanup(input),
    64  		Version:           version,
    65  		SilenceUsage:      true,
    66  	}
    67  
    68  	rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
    69  	rootCmd.Flags().BoolVar(&input.validate, "validate", false, "validate workflows")
    70  	rootCmd.Flags().BoolVar(&input.strict, "strict", false, "use strict workflow schema")
    71  	rootCmd.Flags().BoolP("list", "l", false, "list workflows")
    72  	rootCmd.Flags().BoolP("graph", "g", false, "draw workflows")
    73  	rootCmd.Flags().StringP("job", "j", "", "run a specific job ID")
    74  	rootCmd.Flags().BoolP("bug-report", "", false, "Display system information for bug report")
    75  	rootCmd.Flags().BoolP("man-page", "", false, "Print a generated manual page to stdout")
    76  
    77  	rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
    78  	rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
    79  	rootCmd.Flags().StringArrayVar(&input.vars, "var", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)")
    80  	rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
    81  	rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
    82  	rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
    83  	rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
    84  	rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
    85  	rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present")
    86  	rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present")
    87  	rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
    88  	rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
    89  	rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch")
    90  	rootCmd.Flags().BoolVar(&input.privileged, "privileged", false, "use privileged mode")
    91  	rootCmd.Flags().StringVar(&input.usernsMode, "userns", "", "user namespace to use")
    92  	rootCmd.Flags().BoolVar(&input.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
    93  	rootCmd.Flags().StringArrayVarP(&input.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
    94  	rootCmd.Flags().StringArrayVarP(&input.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
    95  	rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure")
    96  	rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com =github/super-linter)")
    97  	rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "replace-ghe-action-token-with-github-com", "", "If you are using replace-ghe-action-with-github-com  and you want to use private actions on GitHub, you have to set personal access token")
    98  	rootCmd.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13")
    99  	rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event")
   100  	rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)")
   101  	rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
   102  	rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
   103  	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
   104  	rootCmd.PersistentFlags().BoolVar(&input.jsonLogger, "json", false, "Output logs in json format")
   105  	rootCmd.PersistentFlags().BoolVar(&input.logPrefixJobID, "log-prefix-job-id", false, "Output the job id within non-json logs instead of the entire name")
   106  	rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
   107  	rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "disable container creation, validates only workflow correctness")
   108  	rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
   109  	rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", ".vars", "file with list of vars to read from (e.g. --var-file .vars)")
   110  	rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
   111  	rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
   112  	rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input")
   113  	rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")
   114  	rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "", "URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket)")
   115  	rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
   116  	rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Only use this when using GitHub Enterprise Server.")
   117  	rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")
   118  	rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.")
   119  	rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.")
   120  	rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Use actions/checkout instead of copying local files into container")
   121  	rootCmd.PersistentFlags().BoolVarP(&input.noCacheServer, "no-cache-server", "", false, "Disable cache server")
   122  	rootCmd.PersistentFlags().StringVarP(&input.cacheServerPath, "cache-server-path", "", filepath.Join(CacheHomeDir, "actcache"), "Defines the path where the cache server stores caches.")
   123  	rootCmd.PersistentFlags().StringVarP(&input.cacheServerExternalURL, "cache-server-external-url", "", "", "Defines the external URL for if the cache server is behind a proxy. e.g.: https://act-cache-server.example.com. Be careful that there is no trailing slash.")
   124  	rootCmd.PersistentFlags().StringVarP(&input.cacheServerAddr, "cache-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the cache server binds.")
   125  	rootCmd.PersistentFlags().Uint16VarP(&input.cacheServerPort, "cache-server-port", "", 0, "Defines the port where the artifact server listens. 0 means a randomly available port.")
   126  	rootCmd.PersistentFlags().StringVarP(&input.actionCachePath, "action-cache-path", "", filepath.Join(CacheHomeDir, "act"), "Defines the path where the actions get cached and host workspaces created.")
   127  	rootCmd.PersistentFlags().BoolVarP(&input.actionOfflineMode, "action-offline-mode", "", false, "If action contents exists, it will not be fetch and pull again. If turn on this, will turn off force pull")
   128  	rootCmd.PersistentFlags().StringVarP(&input.networkName, "network", "", "host", "Sets a docker network name. Defaults to host.")
   129  	rootCmd.PersistentFlags().BoolVarP(&input.useNewActionCache, "use-new-action-cache", "", false, "Enable using the new Action Cache for storing Actions locally")
   130  	rootCmd.PersistentFlags().StringArrayVarP(&input.localRepository, "local-repository", "", []string{}, "Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols)")
   131  	rootCmd.PersistentFlags().BoolVar(&input.listOptions, "list-options", false, "Print a json structure of compatible options")
   132  	rootCmd.PersistentFlags().IntVar(&input.concurrentJobs, "concurrent-jobs", 0, "Maximum number of concurrent jobs to run. Default is the number of CPUs available.")
   133  	rootCmd.SetArgs(args())
   134  	return rootCmd
   135  }
   136  
   137  // Return locations where Act's config can be found in order: XDG spec, .actrc in HOME directory, .actrc in invocation directory
   138  func configLocations() []string {
   139  	configFileName := ".actrc"
   140  
   141  	homePath := filepath.Join(UserHomeDir, configFileName)
   142  	invocationPath := filepath.Join(".", configFileName)
   143  
   144  	// Though named xdg, adrg's lib support macOS and Windows config paths as well
   145  	// It also takes cares of creating the parent folder so we don't need to bother later
   146  	specPath, err := xdg.ConfigFile("act/actrc")
   147  	if err != nil {
   148  		specPath = homePath
   149  	}
   150  
   151  	// This order should be enforced since the survey part relies on it
   152  	return []string{specPath, homePath, invocationPath}
   153  }
   154  
   155  func args() []string {
   156  	actrc := configLocations()
   157  
   158  	args := make([]string, 0)
   159  	for _, f := range actrc {
   160  		args = append(args, readArgsFile(f, true)...)
   161  	}
   162  
   163  	args = append(args, os.Args[1:]...)
   164  	return args
   165  }
   166  
   167  func bugReport(ctx context.Context, version string) error {
   168  	sprintf := func(key, val string) string {
   169  		return fmt.Sprintf("%-24s%s\n", key, val)
   170  	}
   171  
   172  	report := sprintf("act version:", version)
   173  	report += sprintf("GOOS:", runtime.GOOS)
   174  	report += sprintf("GOARCH:", runtime.GOARCH)
   175  	report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU()))
   176  
   177  	var dockerHost string
   178  	var exists bool
   179  	if dockerHost, exists = os.LookupEnv("DOCKER_HOST"); !exists {
   180  		dockerHost = "DOCKER_HOST environment variable is not set"
   181  	} else if dockerHost == "" {
   182  		dockerHost = "DOCKER_HOST environment variable is empty."
   183  	}
   184  
   185  	report += sprintf("Docker host:", dockerHost)
   186  	report += fmt.Sprintln("Sockets found:")
   187  	for _, p := range container.CommonSocketLocations {
   188  		if _, err := os.Lstat(os.ExpandEnv(p)); err != nil {
   189  			continue
   190  		} else if _, err := os.Stat(os.ExpandEnv(p)); err != nil {
   191  			report += fmt.Sprintf("\t%s(broken)\n", p)
   192  		} else {
   193  			report += fmt.Sprintf("\t%s\n", p)
   194  		}
   195  	}
   196  
   197  	report += sprintf("Config files:", "")
   198  	for _, c := range configLocations() {
   199  		args := readArgsFile(c, false)
   200  		if len(args) > 0 {
   201  			report += fmt.Sprintf("\t%s:\n", c)
   202  			for _, l := range args {
   203  				report += fmt.Sprintf("\t\t%s\n", l)
   204  			}
   205  		}
   206  	}
   207  
   208  	vcs, ok := debug.ReadBuildInfo()
   209  	if ok && vcs != nil {
   210  		report += fmt.Sprintln("Build info:")
   211  		vcs := *vcs
   212  		report += sprintf("\tGo version:", vcs.GoVersion)
   213  		report += sprintf("\tModule path:", vcs.Path)
   214  		report += sprintf("\tMain version:", vcs.Main.Version)
   215  		report += sprintf("\tMain path:", vcs.Main.Path)
   216  		report += sprintf("\tMain checksum:", vcs.Main.Sum)
   217  
   218  		report += fmt.Sprintln("\tBuild settings:")
   219  		for _, set := range vcs.Settings {
   220  			report += sprintf(fmt.Sprintf("\t\t%s:", set.Key), set.Value)
   221  		}
   222  	}
   223  
   224  	info, err := container.GetHostInfo(ctx)
   225  	if err != nil {
   226  		fmt.Println(report)
   227  		return err
   228  	}
   229  
   230  	report += fmt.Sprintln("Docker Engine:")
   231  
   232  	report += sprintf("\tEngine version:", info.ServerVersion)
   233  	report += sprintf("\tEngine runtime:", info.DefaultRuntime)
   234  	report += sprintf("\tCgroup version:", info.CgroupVersion)
   235  	report += sprintf("\tCgroup driver:", info.CgroupDriver)
   236  	report += sprintf("\tStorage driver:", info.Driver)
   237  	report += sprintf("\tRegistry URI:", info.IndexServerAddress)
   238  
   239  	report += sprintf("\tOS:", info.OperatingSystem)
   240  	report += sprintf("\tOS type:", info.OSType)
   241  	report += sprintf("\tOS version:", info.OSVersion)
   242  	report += sprintf("\tOS arch:", info.Architecture)
   243  	report += sprintf("\tOS kernel:", info.KernelVersion)
   244  	report += sprintf("\tOS CPU:", fmt.Sprint(info.NCPU))
   245  	report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024))
   246  
   247  	report += fmt.Sprintln("\tSecurity options:")
   248  	for _, secopt := range info.SecurityOptions {
   249  		report += fmt.Sprintf("\t\t%s\n", secopt)
   250  	}
   251  
   252  	fmt.Println(report)
   253  	return nil
   254  }
   255  
   256  func generateManPage(cmd *cobra.Command) error {
   257  	header := &doc.GenManHeader{
   258  		Title:   "act",
   259  		Section: "1",
   260  		Source:  fmt.Sprintf("act %s", cmd.Version),
   261  	}
   262  	buf := new(bytes.Buffer)
   263  	cobra.CheckErr(doc.GenMan(cmd, header, buf))
   264  	fmt.Print(buf.String())
   265  	return nil
   266  }
   267  
   268  func listOptions(cmd *cobra.Command) error {
   269  	flags := []Flag{}
   270  	cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
   271  		flags = append(flags, Flag{Name: f.Name, Default: f.DefValue, Description: f.Usage, Type: f.Value.Type()})
   272  	})
   273  	a, err := json.Marshal(flags)
   274  	fmt.Println(string(a))
   275  	return err
   276  }
   277  
   278  func readArgsFile(file string, split bool) []string {
   279  	args := make([]string, 0)
   280  	f, err := os.Open(file)
   281  	if err != nil {
   282  		return args
   283  	}
   284  	defer func() {
   285  		err := f.Close()
   286  		if err != nil {
   287  			log.Errorf("Failed to close args file: %v", err)
   288  		}
   289  	}()
   290  	scanner := bufio.NewScanner(f)
   291  	scanner.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow
   292  	for scanner.Scan() {
   293  		arg := os.ExpandEnv(strings.TrimSpace(scanner.Text()))
   294  
   295  		if strings.HasPrefix(arg, "-") && split {
   296  			args = append(args, regexp.MustCompile(`\s`).Split(arg, 2)...)
   297  		} else if !split {
   298  			args = append(args, arg)
   299  		}
   300  	}
   301  	return args
   302  }
   303  
   304  func setup(_ *Input) func(*cobra.Command, []string) {
   305  	return func(cmd *cobra.Command, _ []string) {
   306  		verbose, _ := cmd.Flags().GetBool("verbose")
   307  		if verbose {
   308  			log.SetLevel(log.DebugLevel)
   309  		}
   310  		loadVersionNotices(cmd.Version)
   311  	}
   312  }
   313  
   314  func cleanup(inputs *Input) func(*cobra.Command, []string) {
   315  	return func(_ *cobra.Command, _ []string) {
   316  		displayNotices(inputs)
   317  	}
   318  }
   319  
   320  func parseEnvs(env []string) map[string]string {
   321  	envs := make(map[string]string, len(env))
   322  	for _, envVar := range env {
   323  		e := strings.SplitN(envVar, `=`, 2)
   324  		if len(e) == 2 {
   325  			envs[e[0]] = e[1]
   326  		} else {
   327  			envs[e[0]] = ""
   328  		}
   329  	}
   330  	return envs
   331  }
   332  
   333  func readYamlFile(file string) (map[string]string, error) {
   334  	content, err := os.ReadFile(file)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  	ret := map[string]string{}
   339  	if err = yaml.Unmarshal(content, &ret); err != nil {
   340  		return nil, err
   341  	}
   342  	return ret, nil
   343  }
   344  
   345  func readEnvs(path string, envs map[string]string) bool {
   346  	return readEnvsEx(path, envs, false)
   347  }
   348  
   349  func readEnvsEx(path string, envs map[string]string, caseInsensitive bool) bool {
   350  	if _, err := os.Stat(path); err == nil {
   351  		var env map[string]string
   352  		if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" {
   353  			env, err = readYamlFile(path)
   354  		} else {
   355  			env, err = godotenv.Read(path)
   356  		}
   357  		if err != nil {
   358  			log.Fatalf("Error loading from %s: %v", path, err)
   359  		}
   360  		for k, v := range env {
   361  			if caseInsensitive {
   362  				k = strings.ToUpper(k)
   363  			}
   364  			if _, ok := envs[k]; !ok {
   365  				envs[k] = v
   366  			}
   367  		}
   368  		return true
   369  	}
   370  	return false
   371  }
   372  
   373  func parseMatrix(matrix []string) map[string]map[string]bool {
   374  	// each matrix entry should be of the form - string:string
   375  	r := regexp.MustCompile(":")
   376  	matrixes := make(map[string]map[string]bool)
   377  	for _, m := range matrix {
   378  		matrix := r.Split(m, 2)
   379  		if len(matrix) < 2 {
   380  			log.Fatalf("Invalid matrix format. Failed to parse %s", m)
   381  		}
   382  		if _, ok := matrixes[matrix[0]]; !ok {
   383  			matrixes[matrix[0]] = make(map[string]bool)
   384  		}
   385  		matrixes[matrix[0]][matrix[1]] = true
   386  	}
   387  	return matrixes
   388  }
   389  
   390  //nolint:gocyclo
   391  func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
   392  	return func(cmd *cobra.Command, args []string) error {
   393  		if input.jsonLogger {
   394  			log.SetFormatter(&log.JSONFormatter{})
   395  		}
   396  
   397  		if ok, _ := cmd.Flags().GetBool("bug-report"); ok {
   398  			ctx, cancel := common.EarlyCancelContext(ctx)
   399  			defer cancel()
   400  			return bugReport(ctx, cmd.Version)
   401  		}
   402  		if ok, _ := cmd.Flags().GetBool("man-page"); ok {
   403  			return generateManPage(cmd)
   404  		}
   405  		if input.listOptions {
   406  			return listOptions(cmd)
   407  		}
   408  
   409  		if ret, err := container.GetSocketAndHost(input.containerDaemonSocket); err != nil {
   410  			log.Warnf("Couldn't get a valid docker connection: %+v", err)
   411  		} else {
   412  			os.Setenv("DOCKER_HOST", ret.Host)
   413  			input.containerDaemonSocket = ret.Socket
   414  			log.Infof("Using docker host '%s', and daemon socket '%s'", ret.Host, ret.Socket)
   415  		}
   416  
   417  		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
   418  			l := log.New()
   419  			l.SetFormatter(&log.TextFormatter{
   420  				DisableQuote:     true,
   421  				DisableTimestamp: true,
   422  			})
   423  			l.Warnf(" \U000026A0 You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. \U000026A0 \n")
   424  		}
   425  
   426  		log.Debugf("Loading environment from %s", input.Envfile())
   427  		envs := parseEnvs(input.envs)
   428  		_ = readEnvs(input.Envfile(), envs)
   429  
   430  		log.Debugf("Loading action inputs from %s", input.Inputfile())
   431  		inputs := parseEnvs(input.inputs)
   432  		_ = readEnvs(input.Inputfile(), inputs)
   433  
   434  		log.Debugf("Loading secrets from %s", input.Secretfile())
   435  		secrets := newSecrets(input.secrets)
   436  		_ = readEnvsEx(input.Secretfile(), secrets, true)
   437  
   438  		if _, hasGitHubToken := secrets["GITHUB_TOKEN"]; !hasGitHubToken {
   439  			ctx, cancel := common.EarlyCancelContext(ctx)
   440  			defer cancel()
   441  			secrets["GITHUB_TOKEN"], _ = gh.GetToken(ctx, "")
   442  		}
   443  
   444  		log.Debugf("Loading vars from %s", input.Varfile())
   445  		vars := newSecrets(input.vars)
   446  		_ = readEnvs(input.Varfile(), vars)
   447  
   448  		matrixes := parseMatrix(input.matrix)
   449  		log.Debugf("Evaluated matrix inclusions: %v", matrixes)
   450  
   451  		planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse, input.strict)
   452  		if err != nil {
   453  			return err
   454  		}
   455  
   456  		jobID, err := cmd.Flags().GetString("job")
   457  		if err != nil {
   458  			return err
   459  		}
   460  
   461  		// check if we should just list the workflows
   462  		list, err := cmd.Flags().GetBool("list")
   463  		if err != nil {
   464  			return err
   465  		}
   466  
   467  		// check if we should just validate the workflows
   468  		if input.validate {
   469  			return err
   470  		}
   471  
   472  		// check if we should just draw the graph
   473  		graph, err := cmd.Flags().GetBool("graph")
   474  		if err != nil {
   475  			return err
   476  		}
   477  
   478  		// collect all events from loaded workflows
   479  		events := planner.GetEvents()
   480  
   481  		// plan with filtered jobs - to be used for filtering only
   482  		var filterPlan *model.Plan
   483  
   484  		// Determine the event name to be filtered
   485  		var filterEventName string
   486  
   487  		if len(args) > 0 {
   488  			log.Debugf("Using first passed in arguments event for filtering: %s", args[0])
   489  			filterEventName = args[0]
   490  		} else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
   491  			// set default event type to first event from many available
   492  			// this way user dont have to specify the event.
   493  			log.Debugf("Using first detected workflow event for filtering: %s", events[0])
   494  			filterEventName = events[0]
   495  		}
   496  
   497  		var plannerErr error
   498  		if jobID != "" {
   499  			log.Debugf("Preparing plan with a job: %s", jobID)
   500  			filterPlan, plannerErr = planner.PlanJob(jobID)
   501  		} else if filterEventName != "" {
   502  			log.Debugf("Preparing plan for a event: %s", filterEventName)
   503  			filterPlan, plannerErr = planner.PlanEvent(filterEventName)
   504  		} else {
   505  			log.Debugf("Preparing plan with all jobs")
   506  			filterPlan, plannerErr = planner.PlanAll()
   507  		}
   508  		if filterPlan == nil && plannerErr != nil {
   509  			return plannerErr
   510  		}
   511  
   512  		if list {
   513  			err = printList(filterPlan)
   514  			if err != nil {
   515  				return err
   516  			}
   517  			return plannerErr
   518  		}
   519  
   520  		if graph {
   521  			err = drawGraph(filterPlan)
   522  			if err != nil {
   523  				return err
   524  			}
   525  			return plannerErr
   526  		}
   527  
   528  		// plan with triggered jobs
   529  		var plan *model.Plan
   530  
   531  		// Determine the event name to be triggered
   532  		var eventName string
   533  
   534  		if len(args) > 0 {
   535  			log.Debugf("Using first passed in arguments event: %s", args[0])
   536  			eventName = args[0]
   537  		} else if len(events) == 1 && len(events[0]) > 0 {
   538  			log.Debugf("Using the only detected workflow event: %s", events[0])
   539  			eventName = events[0]
   540  		} else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
   541  			// set default event type to first event from many available
   542  			// this way user dont have to specify the event.
   543  			log.Debugf("Using first detected workflow event: %s", events[0])
   544  			eventName = events[0]
   545  		} else {
   546  			log.Debugf("Using default workflow event: push")
   547  			eventName = "push"
   548  		}
   549  
   550  		// build the plan for this run
   551  		if jobID != "" {
   552  			log.Debugf("Planning job: %s", jobID)
   553  			plan, plannerErr = planner.PlanJob(jobID)
   554  		} else {
   555  			log.Debugf("Planning jobs for event: %s", eventName)
   556  			plan, plannerErr = planner.PlanEvent(eventName)
   557  		}
   558  		if plan != nil {
   559  			if len(plan.Stages) == 0 {
   560  				plannerErr = fmt.Errorf("Could not find any stages to run. View the valid jobs with `act --list`. Use `act --help` to find how to filter by Job ID/Workflow/Event Name")
   561  			}
   562  		}
   563  		if plan == nil && plannerErr != nil {
   564  			return plannerErr
   565  		}
   566  
   567  		// check to see if the main branch was defined
   568  		defaultbranch, err := cmd.Flags().GetString("defaultbranch")
   569  		if err != nil {
   570  			return err
   571  		}
   572  
   573  		// Check if platforms flag is set, if not, run default image survey
   574  		if len(input.platforms) == 0 {
   575  			cfgFound := false
   576  			cfgLocations := configLocations()
   577  			for _, v := range cfgLocations {
   578  				_, err := os.Stat(v)
   579  				if os.IsExist(err) {
   580  					cfgFound = true
   581  				}
   582  			}
   583  			if !cfgFound && len(cfgLocations) > 0 {
   584  				// The first config location refers to the global config folder one
   585  				if err := defaultImageSurvey(cfgLocations[0]); err != nil {
   586  					log.Fatal(err)
   587  				}
   588  				input.platforms = readArgsFile(cfgLocations[0], true)
   589  			}
   590  		}
   591  		deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`."
   592  		if input.privileged {
   593  			log.Warnf(deprecationWarning, "privileged", "--privileged")
   594  		}
   595  		if len(input.usernsMode) > 0 {
   596  			log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode))
   597  		}
   598  		if len(input.containerCapAdd) > 0 {
   599  			log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd))
   600  		}
   601  		if len(input.containerCapDrop) > 0 {
   602  			log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop))
   603  		}
   604  
   605  		// run the plan
   606  		config := &runner.Config{
   607  			Actor:                              input.actor,
   608  			EventName:                          eventName,
   609  			EventPath:                          input.EventPath(),
   610  			DefaultBranch:                      defaultbranch,
   611  			ForcePull:                          !input.actionOfflineMode && input.forcePull,
   612  			ForceRebuild:                       input.forceRebuild,
   613  			ReuseContainers:                    input.reuseContainers,
   614  			Workdir:                            input.Workdir(),
   615  			ActionCacheDir:                     input.actionCachePath,
   616  			ActionOfflineMode:                  input.actionOfflineMode,
   617  			BindWorkdir:                        input.bindWorkdir,
   618  			LogOutput:                          !input.noOutput,
   619  			JSONLogger:                         input.jsonLogger,
   620  			LogPrefixJobID:                     input.logPrefixJobID,
   621  			Env:                                envs,
   622  			Secrets:                            secrets,
   623  			Vars:                               vars,
   624  			Inputs:                             inputs,
   625  			Token:                              secrets["GITHUB_TOKEN"],
   626  			InsecureSecrets:                    input.insecureSecrets,
   627  			Platforms:                          input.newPlatforms(),
   628  			Privileged:                         input.privileged,
   629  			UsernsMode:                         input.usernsMode,
   630  			ContainerArchitecture:              input.containerArchitecture,
   631  			ContainerDaemonSocket:              input.containerDaemonSocket,
   632  			ContainerOptions:                   input.containerOptions,
   633  			UseGitIgnore:                       input.useGitIgnore,
   634  			GitHubInstance:                     input.githubInstance,
   635  			ContainerCapAdd:                    input.containerCapAdd,
   636  			ContainerCapDrop:                   input.containerCapDrop,
   637  			AutoRemove:                         input.autoRemove,
   638  			ArtifactServerPath:                 input.artifactServerPath,
   639  			ArtifactServerAddr:                 input.artifactServerAddr,
   640  			ArtifactServerPort:                 input.artifactServerPort,
   641  			NoSkipCheckout:                     input.noSkipCheckout,
   642  			RemoteName:                         input.remoteName,
   643  			ReplaceGheActionWithGithubCom:      input.replaceGheActionWithGithubCom,
   644  			ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom,
   645  			Matrix:                             matrixes,
   646  			ContainerNetworkMode:               docker_container.NetworkMode(input.networkName),
   647  			ConcurrentJobs:                     input.concurrentJobs,
   648  		}
   649  		if input.useNewActionCache || len(input.localRepository) > 0 {
   650  			if input.actionOfflineMode {
   651  				config.ActionCache = &runner.GoGitActionCacheOfflineMode{
   652  					Parent: runner.GoGitActionCache{
   653  						Path: config.ActionCacheDir,
   654  					},
   655  				}
   656  			} else {
   657  				config.ActionCache = &runner.GoGitActionCache{
   658  					Path: config.ActionCacheDir,
   659  				}
   660  			}
   661  			if len(input.localRepository) > 0 {
   662  				localRepositories := map[string]string{}
   663  				for _, l := range input.localRepository {
   664  					k, v, _ := strings.Cut(l, "=")
   665  					localRepositories[k] = v
   666  				}
   667  				config.ActionCache = &runner.LocalRepositoryCache{
   668  					Parent:            config.ActionCache,
   669  					LocalRepositories: localRepositories,
   670  					CacheDirCache:     map[string]string{},
   671  				}
   672  			}
   673  		}
   674  		r, err := runner.New(config)
   675  		if err != nil {
   676  			return err
   677  		}
   678  
   679  		cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort)
   680  
   681  		const cacheURLKey = "ACTIONS_CACHE_URL"
   682  		var cacheHandler *artifactcache.Handler
   683  		if !input.noCacheServer && envs[cacheURLKey] == "" {
   684  			var err error
   685  			cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerExternalURL, input.cacheServerAddr, input.cacheServerPort, common.Logger(ctx))
   686  			if err != nil {
   687  				return err
   688  			}
   689  			envs[cacheURLKey] = cacheHandler.ExternalURL() + "/"
   690  		}
   691  
   692  		ctx = common.WithDryrun(ctx, input.dryrun)
   693  		if watch, err := cmd.Flags().GetBool("watch"); err != nil {
   694  			return err
   695  		} else if watch {
   696  			err = watchAndRun(ctx, r.NewPlanExecutor(plan))
   697  			if err != nil {
   698  				return err
   699  			}
   700  			return plannerErr
   701  		}
   702  
   703  		executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
   704  			cancel()
   705  			_ = cacheHandler.Close()
   706  			return nil
   707  		})
   708  		err = executor(ctx)
   709  		if err != nil {
   710  			return err
   711  		}
   712  		return plannerErr
   713  	}
   714  }
   715  
   716  func defaultImageSurvey(actrc string) error {
   717  	var answer string
   718  	confirmation := &survey.Select{
   719  		Message: "Please choose the default image you want to use with act:\n  - Large size image: ca. 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images\n  - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most actions\n  - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions\n\nDefault image and other options can be changed manually in " + configLocations()[0] + " (please refer to https://nektosact.com/usage/index.html?highlight=configur#configuration-file for additional information about file structure)",
   720  		Help:    "If you want to know why act asks you that, please go to https://github.com/nektos/act/issues/107",
   721  		Default: "Medium",
   722  		Options: []string{"Large", "Medium", "Micro"},
   723  	}
   724  
   725  	err := survey.AskOne(confirmation, &answer)
   726  	if err != nil {
   727  		return err
   728  	}
   729  
   730  	var option string
   731  	switch answer {
   732  	case "Large":
   733  		option = "-P ubuntu-latest=catthehacker/ubuntu:full-latest\n-P ubuntu-22.04=catthehacker/ubuntu:full-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:full-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:full-18.04\n"
   734  	case "Medium":
   735  		option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n"
   736  	case "Micro":
   737  		option = "-P ubuntu-latest=node:16-buster-slim\n-P ubuntu-22.04=node:16-bullseye-slim\n-P ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n"
   738  	}
   739  
   740  	f, err := os.Create(actrc)
   741  	if err != nil {
   742  		return err
   743  	}
   744  
   745  	_, err = f.WriteString(option)
   746  	if err != nil {
   747  		_ = f.Close()
   748  		return err
   749  	}
   750  
   751  	err = f.Close()
   752  	if err != nil {
   753  		return err
   754  	}
   755  
   756  	return nil
   757  }
   758  
   759  func watchAndRun(ctx context.Context, fn common.Executor) error {
   760  	dir, err := os.Getwd()
   761  	if err != nil {
   762  		return err
   763  	}
   764  
   765  	ignoreFile := filepath.Join(dir, ".gitignore")
   766  	ignore := &gitignore.GitIgnore{}
   767  	if info, err := os.Stat(ignoreFile); err == nil && !info.IsDir() {
   768  		ignore, err = gitignore.CompileIgnoreFile(ignoreFile)
   769  		if err != nil {
   770  			return fmt.Errorf("compile %q: %w", ignoreFile, err)
   771  		}
   772  	}
   773  
   774  	folderWatcher := fswatch.NewFolderWatcher(
   775  		dir,
   776  		true,
   777  		ignore.MatchesPath,
   778  		2, // 2 seconds
   779  	)
   780  
   781  	folderWatcher.Start()
   782  	defer folderWatcher.Stop()
   783  
   784  	// run once before watching
   785  	if err := fn(ctx); err != nil {
   786  		return err
   787  	}
   788  
   789  	earlyCancelCtx, cancel := common.EarlyCancelContext(ctx)
   790  	defer cancel()
   791  
   792  	for folderWatcher.IsRunning() {
   793  		log.Debugf("Watching %s for changes", dir)
   794  		select {
   795  		case <-earlyCancelCtx.Done():
   796  			return nil
   797  		case changes := <-folderWatcher.ChangeDetails():
   798  			log.Debugf("%s", changes.String())
   799  			if err := fn(ctx); err != nil {
   800  				return err
   801  			}
   802  		}
   803  	}
   804  
   805  	return nil
   806  }