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