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 }