From 9f94da307db20c79392e015302954891208d2000 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 16 Jul 2025 06:15:06 +0000 Subject: [PATCH] chore(cleanup): ACT cli is redundant with Forgejo runner cli (#182) it is not used in the context of forgejo and is mistaken sometimes for being supported. Reviewed-on: https://code.forgejo.org/forgejo/act/pulls/182 Reviewed-by: Michael Kriese Co-authored-by: Earl Warren Co-committed-by: Earl Warren --- cmd/dir.go | 27 -- cmd/graph.go | 38 --- cmd/input.go | 113 ------- cmd/list.go | 107 ------- cmd/notices.go | 140 --------- cmd/platforms.go | 22 -- cmd/root.go | 781 ----------------------------------------------- cmd/secrets.go | 42 --- main.go | 37 --- 9 files changed, 1307 deletions(-) delete mode 100644 cmd/dir.go delete mode 100644 cmd/graph.go delete mode 100644 cmd/input.go delete mode 100644 cmd/list.go delete mode 100644 cmd/notices.go delete mode 100644 cmd/platforms.go delete mode 100644 cmd/root.go delete mode 100644 cmd/secrets.go delete mode 100644 main.go diff --git a/cmd/dir.go b/cmd/dir.go deleted file mode 100644 index e1d24e9a..00000000 --- a/cmd/dir.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "os" - "path/filepath" - - log "github.com/sirupsen/logrus" -) - -var ( - UserHomeDir string - CacheHomeDir string -) - -func init() { - home, err := os.UserHomeDir() - if err != nil { - log.Fatal(err) - } - UserHomeDir = home - - if v := os.Getenv("XDG_CACHE_HOME"); v != "" { - CacheHomeDir = v - } else { - CacheHomeDir = filepath.Join(UserHomeDir, ".cache") - } -} diff --git a/cmd/graph.go b/cmd/graph.go deleted file mode 100644 index b38487be..00000000 --- a/cmd/graph.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/nektos/act/pkg/common" - "github.com/nektos/act/pkg/model" -) - -func drawGraph(plan *model.Plan) error { - drawings := make([]*common.Drawing, 0) - - jobPen := common.NewPen(common.StyleSingleLine, 96) - arrowPen := common.NewPen(common.StyleNoLine, 97) - for i, stage := range plan.Stages { - if i > 0 { - drawings = append(drawings, arrowPen.DrawArrow()) - } - - ids := make([]string, 0) - for _, r := range stage.Runs { - ids = append(ids, r.String()) - } - drawings = append(drawings, jobPen.DrawBoxes(ids...)) - } - - maxWidth := 0 - for _, d := range drawings { - if d.GetWidth() > maxWidth { - maxWidth = d.GetWidth() - } - } - - for _, d := range drawings { - d.Draw(os.Stdout, maxWidth) - } - return nil -} diff --git a/cmd/input.go b/cmd/input.go deleted file mode 100644 index f201dbc2..00000000 --- a/cmd/input.go +++ /dev/null @@ -1,113 +0,0 @@ -package cmd - -import ( - "path/filepath" - - log "github.com/sirupsen/logrus" -) - -// Input contains the input for the root command -type Input struct { - actor string - workdir string - workflowsPath string - autodetectEvent bool - eventPath string - reuseContainers bool - bindWorkdir bool - secrets []string - vars []string - envs []string - inputs []string - platforms []string - dryrun bool - forcePull bool - forceRebuild bool - noOutput bool - envfile string - inputfile string - secretfile string - varfile string - insecureSecrets bool - defaultBranch string - privileged bool - usernsMode string - containerArchitecture string - containerDaemonSocket string - containerOptions string - noWorkflowRecurse bool - useGitIgnore bool - githubInstance string - containerCapAdd []string - containerCapDrop []string - autoRemove bool - artifactServerPath string - artifactServerAddr string - artifactServerPort string - noCacheServer bool - cacheServerPath string - cacheServerAddr string - cacheServerPort uint16 - jsonLogger bool - noSkipCheckout bool - remoteName string - replaceGheActionWithGithubCom []string - replaceGheActionTokenWithGithubCom string - matrix []string - actionCachePath string - actionOfflineMode bool - logPrefixJobID bool - networkName string - useNewActionCache bool - secret string - - localRepository []string -} - -func (i *Input) resolve(path string) string { - basedir, err := filepath.Abs(i.workdir) - if err != nil { - log.Fatal(err) - } - if path == "" { - return path - } - if !filepath.IsAbs(path) { - path = filepath.Join(basedir, path) - } - return path -} - -// Envfile returns path to .env -func (i *Input) Envfile() string { - return i.resolve(i.envfile) -} - -// Secretfile returns path to secrets -func (i *Input) Secretfile() string { - return i.resolve(i.secretfile) -} - -func (i *Input) Varfile() string { - return i.resolve(i.varfile) -} - -// Workdir returns path to workdir -func (i *Input) Workdir() string { - return i.resolve(".") -} - -// WorkflowsPath returns path to workflow file(s) -func (i *Input) WorkflowsPath() string { - return i.resolve(i.workflowsPath) -} - -// EventPath returns the path to events file -func (i *Input) EventPath() string { - return i.resolve(i.eventPath) -} - -// Inputfile returns the path to the input file -func (i *Input) Inputfile() string { - return i.resolve(i.inputfile) -} diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index 15799aab..00000000 --- a/cmd/list.go +++ /dev/null @@ -1,107 +0,0 @@ -package cmd - -import ( - "fmt" - "strconv" - "strings" - - "github.com/nektos/act/pkg/model" -) - -func printList(plan *model.Plan) error { - type lineInfoDef struct { - jobID string - jobName string - stage string - wfName string - wfFile string - events string - } - lineInfos := []lineInfoDef{} - - header := lineInfoDef{ - jobID: "Job ID", - jobName: "Job name", - stage: "Stage", - wfName: "Workflow name", - wfFile: "Workflow file", - events: "Events", - } - - jobs := map[string]bool{} - duplicateJobIDs := false - - jobIDMaxWidth := len(header.jobID) - jobNameMaxWidth := len(header.jobName) - stageMaxWidth := len(header.stage) - wfNameMaxWidth := len(header.wfName) - wfFileMaxWidth := len(header.wfFile) - eventsMaxWidth := len(header.events) - - for i, stage := range plan.Stages { - for _, r := range stage.Runs { - jobID := r.JobID - line := lineInfoDef{ - jobID: jobID, - jobName: r.String(), - stage: strconv.Itoa(i), - wfName: r.Workflow.Name, - wfFile: r.Workflow.File, - events: strings.Join(r.Workflow.On(), `,`), - } - if _, ok := jobs[jobID]; ok { - duplicateJobIDs = true - } else { - jobs[jobID] = true - } - lineInfos = append(lineInfos, line) - if jobIDMaxWidth < len(line.jobID) { - jobIDMaxWidth = len(line.jobID) - } - if jobNameMaxWidth < len(line.jobName) { - jobNameMaxWidth = len(line.jobName) - } - if stageMaxWidth < len(line.stage) { - stageMaxWidth = len(line.stage) - } - if wfNameMaxWidth < len(line.wfName) { - wfNameMaxWidth = len(line.wfName) - } - if wfFileMaxWidth < len(line.wfFile) { - wfFileMaxWidth = len(line.wfFile) - } - if eventsMaxWidth < len(line.events) { - eventsMaxWidth = len(line.events) - } - } - } - - jobIDMaxWidth += 2 - jobNameMaxWidth += 2 - stageMaxWidth += 2 - wfNameMaxWidth += 2 - wfFileMaxWidth += 2 - - fmt.Printf("%*s%*s%*s%*s%*s%*s\n", - -stageMaxWidth, header.stage, - -jobIDMaxWidth, header.jobID, - -jobNameMaxWidth, header.jobName, - -wfNameMaxWidth, header.wfName, - -wfFileMaxWidth, header.wfFile, - -eventsMaxWidth, header.events, - ) - for _, line := range lineInfos { - fmt.Printf("%*s%*s%*s%*s%*s%*s\n", - -stageMaxWidth, line.stage, - -jobIDMaxWidth, line.jobID, - -jobNameMaxWidth, line.jobName, - -wfNameMaxWidth, line.wfName, - -wfFileMaxWidth, line.wfFile, - -eventsMaxWidth, line.events, - ) - } - if duplicateJobIDs { - fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n") - } - return nil -} diff --git a/cmd/notices.go b/cmd/notices.go deleted file mode 100644 index a912bd9f..00000000 --- a/cmd/notices.go +++ /dev/null @@ -1,140 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - log "github.com/sirupsen/logrus" -) - -type Notice struct { - Level string `json:"level"` - Message string `json:"message"` -} - -func displayNotices(input *Input) { - select { - case notices := <-noticesLoaded: - if len(notices) > 0 { - noticeLogger := log.New() - if input.jsonLogger { - noticeLogger.SetFormatter(&log.JSONFormatter{}) - } else { - noticeLogger.SetFormatter(&log.TextFormatter{ - DisableQuote: true, - DisableTimestamp: true, - PadLevelText: true, - }) - } - - fmt.Printf("\n") - for _, notice := range notices { - level, err := log.ParseLevel(notice.Level) - if err != nil { - level = log.InfoLevel - } - noticeLogger.Log(level, notice.Message) - } - } - case <-time.After(time.Second * 1): - log.Debugf("Timeout waiting for notices") - } -} - -var noticesLoaded = make(chan []Notice) - -func loadVersionNotices(version string) { - go func() { - noticesLoaded <- getVersionNotices(version) - }() -} - -const NoticeURL = "https://api.nektosact.com/notices" - -func getVersionNotices(version string) []Notice { - if os.Getenv("ACT_DISABLE_VERSION_CHECK") == "1" { - return nil - } - - noticeURL, err := url.Parse(NoticeURL) - if err != nil { - log.Error(err) - return nil - } - query := noticeURL.Query() - query.Add("os", runtime.GOOS) - query.Add("arch", runtime.GOARCH) - query.Add("version", version) - - noticeURL.RawQuery = query.Encode() - - client := &http.Client{} - req, err := http.NewRequest("GET", noticeURL.String(), nil) - if err != nil { - log.Debug(err) - return nil - } - - etag := loadNoticesEtag() - if etag != "" { - log.Debugf("Conditional GET for notices etag=%s", etag) - req.Header.Set("If-None-Match", etag) - } - - resp, err := client.Do(req) - if err != nil { - log.Debug(err) - return nil - } - - newEtag := resp.Header.Get("Etag") - if newEtag != "" { - log.Debugf("Saving notices etag=%s", newEtag) - saveNoticesEtag(newEtag) - } - - defer resp.Body.Close() - notices := []Notice{} - if resp.StatusCode == 304 { - log.Debug("No new notices") - return nil - } - if err := json.NewDecoder(resp.Body).Decode(¬ices); err != nil { - log.Debug(err) - return nil - } - - return notices -} - -func loadNoticesEtag() string { - p := etagPath() - content, err := os.ReadFile(p) - if err != nil { - log.Debugf("Unable to load etag from %s: %e", p, err) - } - return strings.TrimSuffix(string(content), "\n") -} - -func saveNoticesEtag(etag string) { - p := etagPath() - err := os.WriteFile(p, []byte(strings.TrimSuffix(etag, "\n")), 0o600) - if err != nil { - log.Debugf("Unable to save etag to %s: %e", p, err) - } -} - -func etagPath() string { - dir := filepath.Join(CacheHomeDir, "act") - if err := os.MkdirAll(dir, 0o777); err != nil { - log.Fatal(err) - } - return filepath.Join(dir, ".notices.etag") -} diff --git a/cmd/platforms.go b/cmd/platforms.go deleted file mode 100644 index 45724d75..00000000 --- a/cmd/platforms.go +++ /dev/null @@ -1,22 +0,0 @@ -package cmd - -import ( - "strings" -) - -func (i *Input) newPlatforms() map[string]string { - platforms := map[string]string{ - "ubuntu-latest": "node:16-buster-slim", - "ubuntu-22.04": "node:16-bullseye-slim", - "ubuntu-20.04": "node:16-buster-slim", - "ubuntu-18.04": "node:16-buster-slim", - } - - for _, p := range i.platforms { - pParts := strings.Split(p, "=") - if len(pParts) == 2 { - platforms[strings.ToLower(pParts[0])] = pParts[1] - } - } - return platforms -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 505d1e7a..00000000 --- a/cmd/root.go +++ /dev/null @@ -1,781 +0,0 @@ -package cmd - -import ( - "bufio" - "context" - "fmt" - "os" - "path/filepath" - "regexp" - "runtime" - "runtime/debug" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/adrg/xdg" - "github.com/andreaskoch/go-fswatch" - docker_container "github.com/docker/docker/api/types/container" - "github.com/joho/godotenv" - gitignore "github.com/sabhiram/go-gitignore" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - - "github.com/nektos/act/pkg/artifactcache" - "github.com/nektos/act/pkg/artifacts" - "github.com/nektos/act/pkg/common" - "github.com/nektos/act/pkg/container" - "github.com/nektos/act/pkg/model" - "github.com/nektos/act/pkg/runner" -) - -// Execute is the entry point to running the CLI -func Execute(ctx context.Context, version string) { - input := new(Input) - var rootCmd = &cobra.Command{ - 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\"", - Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.", - Args: cobra.MaximumNArgs(1), - RunE: newRunCommand(ctx, input), - PersistentPreRun: setup(input), - PersistentPostRun: cleanup(input), - Version: version, - SilenceUsage: true, - } - rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change") - rootCmd.Flags().BoolP("list", "l", false, "list workflows") - rootCmd.Flags().BoolP("graph", "g", false, "draw workflows") - rootCmd.Flags().StringP("job", "j", "", "run a specific job ID") - rootCmd.Flags().BoolP("bug-report", "", false, "Display system information for bug report") - - rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo") - 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)") - rootCmd.Flags().StringArrayVar(&input.vars, "var", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)") - rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)") - rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)") - 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)") - rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") - rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") - rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present") - rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present") - rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow") - rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file") - rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch") - rootCmd.Flags().BoolVar(&input.privileged, "privileged", false, "use privileged mode") - rootCmd.Flags().StringVar(&input.usernsMode, "userns", "", "user namespace to use") - rootCmd.Flags().BoolVar(&input.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container") - rootCmd.Flags().StringArrayVarP(&input.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)") - rootCmd.Flags().StringArrayVarP(&input.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)") - rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure") - 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)") - 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") - rootCmd.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13") - rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event") - rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)") - rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag") - rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory") - rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") - rootCmd.PersistentFlags().BoolVar(&input.jsonLogger, "json", false, "Output logs in json format") - rootCmd.PersistentFlags().BoolVar(&input.logPrefixJobID, "log-prefix-job-id", false, "Output the job id within non-json logs instead of the entire name") - rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps") - rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "disable container creation, validates only workflow correctness") - rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)") - rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", ".vars", "file with list of vars to read from (e.g. --var-file .vars)") - rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.") - rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers") - rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input") - 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.") - 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)") - rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition") - 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.") - 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.") - rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.") - rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.") - rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout") - rootCmd.PersistentFlags().BoolVarP(&input.noCacheServer, "no-cache-server", "", false, "Disable cache server") - rootCmd.PersistentFlags().StringVarP(&input.cacheServerPath, "cache-server-path", "", filepath.Join(CacheHomeDir, "actcache"), "Defines the path where the cache server stores caches.") - rootCmd.PersistentFlags().StringVarP(&input.cacheServerAddr, "cache-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the cache server binds.") - rootCmd.PersistentFlags().Uint16VarP(&input.cacheServerPort, "cache-server-port", "", 0, "Defines the port where the artifact server listens. 0 means a randomly available port.") - rootCmd.PersistentFlags().StringVarP(&input.actionCachePath, "action-cache-path", "", filepath.Join(CacheHomeDir, "act"), "Defines the path where the actions get cached and host workspaces created.") - 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") - rootCmd.PersistentFlags().StringVarP(&input.networkName, "network", "", "host", "Sets a docker network name. Defaults to host.") - rootCmd.PersistentFlags().BoolVarP(&input.useNewActionCache, "use-new-action-cache", "", false, "Enable using the new Action Cache for storing Actions locally") - 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)") - rootCmd.SetArgs(args()) - - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } -} - -// Return locations where Act's config can be found in order: XDG spec, .actrc in HOME directory, .actrc in invocation directory -func configLocations() []string { - configFileName := ".actrc" - - homePath := filepath.Join(UserHomeDir, configFileName) - invocationPath := filepath.Join(".", configFileName) - - // Though named xdg, adrg's lib support macOS and Windows config paths as well - // It also takes cares of creating the parent folder so we don't need to bother later - specPath, err := xdg.ConfigFile("act/actrc") - if err != nil { - specPath = homePath - } - - // This order should be enforced since the survey part relies on it - return []string{specPath, homePath, invocationPath} -} - -var commonSocketPaths = []string{ - "/var/run/docker.sock", - "/run/podman/podman.sock", - "$HOME/.colima/docker.sock", - "$XDG_RUNTIME_DIR/docker.sock", - "$XDG_RUNTIME_DIR/podman/podman.sock", - `\\.\pipe\docker_engine`, - "$HOME/.docker/run/docker.sock", -} - -// returns socket path or false if not found any -func socketLocation() (string, bool) { - if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists { - return dockerHost, true - } - - for _, p := range commonSocketPaths { - if _, err := os.Lstat(os.ExpandEnv(p)); err == nil { - if strings.HasPrefix(p, `\\.\`) { - return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true - } - return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true - } - } - - return "", false -} - -func args() []string { - actrc := configLocations() - - args := make([]string, 0) - for _, f := range actrc { - args = append(args, readArgsFile(f, true)...) - } - - args = append(args, os.Args[1:]...) - return args -} - -func bugReport(ctx context.Context, version string) error { - sprintf := func(key, val string) string { - return fmt.Sprintf("%-24s%s\n", key, val) - } - - report := sprintf("act version:", version) - report += sprintf("GOOS:", runtime.GOOS) - report += sprintf("GOARCH:", runtime.GOARCH) - report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU())) - - var dockerHost string - var exists bool - if dockerHost, exists = os.LookupEnv("DOCKER_HOST"); !exists { - dockerHost = "DOCKER_HOST environment variable is not set" - } else if dockerHost == "" { - dockerHost = "DOCKER_HOST environment variable is empty." - } - - report += sprintf("Docker host:", dockerHost) - report += fmt.Sprintln("Sockets found:") - for _, p := range commonSocketPaths { - if _, err := os.Lstat(os.ExpandEnv(p)); err != nil { - continue - } else if _, err := os.Stat(os.ExpandEnv(p)); err != nil { - report += fmt.Sprintf("\t%s(broken)\n", p) - } else { - report += fmt.Sprintf("\t%s\n", p) - } - } - - report += sprintf("Config files:", "") - for _, c := range configLocations() { - args := readArgsFile(c, false) - if len(args) > 0 { - report += fmt.Sprintf("\t%s:\n", c) - for _, l := range args { - report += fmt.Sprintf("\t\t%s\n", l) - } - } - } - - vcs, ok := debug.ReadBuildInfo() - if ok && vcs != nil { - report += fmt.Sprintln("Build info:") - vcs := *vcs - report += sprintf("\tGo version:", vcs.GoVersion) - report += sprintf("\tModule path:", vcs.Path) - report += sprintf("\tMain version:", vcs.Main.Version) - report += sprintf("\tMain path:", vcs.Main.Path) - report += sprintf("\tMain checksum:", vcs.Main.Sum) - - report += fmt.Sprintln("\tBuild settings:") - for _, set := range vcs.Settings { - report += sprintf(fmt.Sprintf("\t\t%s:", set.Key), set.Value) - } - } - - info, err := container.GetHostInfo(ctx) - if err != nil { - fmt.Println(report) - return err - } - - report += fmt.Sprintln("Docker Engine:") - - report += sprintf("\tEngine version:", info.ServerVersion) - report += sprintf("\tEngine runtime:", info.DefaultRuntime) - report += sprintf("\tCgroup version:", info.CgroupVersion) - report += sprintf("\tCgroup driver:", info.CgroupDriver) - report += sprintf("\tStorage driver:", info.Driver) - report += sprintf("\tRegistry URI:", info.IndexServerAddress) - - report += sprintf("\tOS:", info.OperatingSystem) - report += sprintf("\tOS type:", info.OSType) - report += sprintf("\tOS version:", info.OSVersion) - report += sprintf("\tOS arch:", info.Architecture) - report += sprintf("\tOS kernel:", info.KernelVersion) - report += sprintf("\tOS CPU:", fmt.Sprint(info.NCPU)) - report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024)) - - report += fmt.Sprintln("\tSecurity options:") - for _, secopt := range info.SecurityOptions { - report += fmt.Sprintf("\t\t%s\n", secopt) - } - - fmt.Println(report) - return nil -} - -func readArgsFile(file string, split bool) []string { - args := make([]string, 0) - f, err := os.Open(file) - if err != nil { - return args - } - defer func() { - err := f.Close() - if err != nil { - log.Errorf("Failed to close args file: %v", err) - } - }() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - arg := os.ExpandEnv(strings.TrimSpace(scanner.Text())) - - if strings.HasPrefix(arg, "-") && split { - args = append(args, regexp.MustCompile(`\s`).Split(arg, 2)...) - } else if !split { - args = append(args, arg) - } - } - return args -} - -func setup(_ *Input) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, _ []string) { - verbose, _ := cmd.Flags().GetBool("verbose") - if verbose { - log.SetLevel(log.DebugLevel) - } - loadVersionNotices(cmd.Version) - } -} - -func cleanup(inputs *Input) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, _ []string) { - displayNotices(inputs) - } -} - -func parseEnvs(env []string) map[string]string { - envs := make(map[string]string, len(env)) - for _, envVar := range env { - e := strings.SplitN(envVar, `=`, 2) - if len(e) == 2 { - envs[e[0]] = e[1] - } else { - envs[e[0]] = "" - } - } - return envs -} - -func readYamlFile(file string) (map[string]string, error) { - content, err := os.ReadFile(file) - if err != nil { - return nil, err - } - ret := map[string]string{} - if err = yaml.Unmarshal(content, &ret); err != nil { - return nil, err - } - return ret, nil -} - -func readEnvs(path string, envs map[string]string) bool { - if _, err := os.Stat(path); err == nil { - var env map[string]string - if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" { - env, err = readYamlFile(path) - } else { - env, err = godotenv.Read(path) - } - if err != nil { - log.Fatalf("Error loading from %s: %v", path, err) - } - for k, v := range env { - if _, ok := envs[k]; !ok { - envs[k] = v - } - } - return true - } - return false -} - -func parseMatrix(matrix []string) map[string]map[string]bool { - // each matrix entry should be of the form - string:string - r := regexp.MustCompile(":") - matrixes := make(map[string]map[string]bool) - for _, m := range matrix { - matrix := r.Split(m, 2) - if len(matrix) < 2 { - log.Fatalf("Invalid matrix format. Failed to parse %s", m) - } - if _, ok := matrixes[matrix[0]]; !ok { - matrixes[matrix[0]] = make(map[string]bool) - } - matrixes[matrix[0]][matrix[1]] = true - } - return matrixes -} - -func isDockerHostURI(daemonPath string) bool { - if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 { - scheme := daemonPath[:protoIndex] - if strings.IndexFunc(scheme, func(r rune) bool { - return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') - }) == -1 { - return true - } - } - return false -} - -//nolint:gocyclo -func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - if input.jsonLogger { - log.SetFormatter(&log.JSONFormatter{}) - } - - if ok, _ := cmd.Flags().GetBool("bug-report"); ok { - return bugReport(ctx, cmd.Version) - } - - // Prefer DOCKER_HOST, don't override it - socketPath, hasDockerHost := os.LookupEnv("DOCKER_HOST") - if !hasDockerHost { - // a - in containerDaemonSocket means don't mount, preserve this value - // otherwise if input.containerDaemonSocket is a filepath don't use it as socketPath - skipMount := input.containerDaemonSocket == "-" || !isDockerHostURI(input.containerDaemonSocket) - if input.containerDaemonSocket != "" && !skipMount { - socketPath = input.containerDaemonSocket - } else { - socket, found := socketLocation() - if !found { - log.Errorln("daemon Docker Engine socket not found and containerDaemonSocket option was not set") - } else { - socketPath = socket - } - if !skipMount { - input.containerDaemonSocket = socketPath - } - } - os.Setenv("DOCKER_HOST", socketPath) - } - - if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" { - l := log.New() - l.SetFormatter(&log.TextFormatter{ - DisableQuote: true, - DisableTimestamp: true, - }) - 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") - } - - log.Debugf("Loading environment from %s", input.Envfile()) - envs := parseEnvs(input.envs) - _ = readEnvs(input.Envfile(), envs) - - log.Debugf("Loading action inputs from %s", input.Inputfile()) - inputs := parseEnvs(input.inputs) - _ = readEnvs(input.Inputfile(), inputs) - - log.Debugf("Loading secrets from %s", input.Secretfile()) - secrets := newSecrets(input.secrets) - _ = readEnvs(input.Secretfile(), secrets) - - log.Debugf("Loading vars from %s", input.Varfile()) - vars := newSecrets(input.vars) - _ = readEnvs(input.Varfile(), vars) - - matrixes := parseMatrix(input.matrix) - log.Debugf("Evaluated matrix inclusions: %v", matrixes) - - planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse) - if err != nil { - return err - } - - jobID, err := cmd.Flags().GetString("job") - if err != nil { - return err - } - - // check if we should just list the workflows - list, err := cmd.Flags().GetBool("list") - if err != nil { - return err - } - - // check if we should just draw the graph - graph, err := cmd.Flags().GetBool("graph") - if err != nil { - return err - } - - // collect all events from loaded workflows - events := planner.GetEvents() - - // plan with filtered jobs - to be used for filtering only - var filterPlan *model.Plan - - // Determine the event name to be filtered - var filterEventName string - - if len(args) > 0 { - log.Debugf("Using first passed in arguments event for filtering: %s", args[0]) - filterEventName = args[0] - } else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 { - // set default event type to first event from many available - // this way user dont have to specify the event. - log.Debugf("Using first detected workflow event for filtering: %s", events[0]) - filterEventName = events[0] - } - - var plannerErr error - if jobID != "" { - log.Debugf("Preparing plan with a job: %s", jobID) - filterPlan, plannerErr = planner.PlanJob(jobID) - } else if filterEventName != "" { - log.Debugf("Preparing plan for a event: %s", filterEventName) - filterPlan, plannerErr = planner.PlanEvent(filterEventName) - } else { - log.Debugf("Preparing plan with all jobs") - filterPlan, plannerErr = planner.PlanAll() - } - if filterPlan == nil && plannerErr != nil { - return plannerErr - } - - if list { - err = printList(filterPlan) - if err != nil { - return err - } - return plannerErr - } - - if graph { - err = drawGraph(filterPlan) - if err != nil { - return err - } - return plannerErr - } - - // plan with triggered jobs - var plan *model.Plan - - // Determine the event name to be triggered - var eventName string - - if len(args) > 0 { - log.Debugf("Using first passed in arguments event: %s", args[0]) - eventName = args[0] - } else if len(events) == 1 && len(events[0]) > 0 { - log.Debugf("Using the only detected workflow event: %s", events[0]) - eventName = events[0] - } else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 { - // set default event type to first event from many available - // this way user dont have to specify the event. - log.Debugf("Using first detected workflow event: %s", events[0]) - eventName = events[0] - } else { - log.Debugf("Using default workflow event: push") - eventName = "push" - } - - // build the plan for this run - if jobID != "" { - log.Debugf("Planning job: %s", jobID) - plan, plannerErr = planner.PlanJob(jobID) - } else { - log.Debugf("Planning jobs for event: %s", eventName) - plan, plannerErr = planner.PlanEvent(eventName) - } - if plan != nil { - if len(plan.Stages) == 0 { - 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") - } - } - if plan == nil && plannerErr != nil { - return plannerErr - } - - // check to see if the main branch was defined - defaultbranch, err := cmd.Flags().GetString("defaultbranch") - if err != nil { - return err - } - - // Check if platforms flag is set, if not, run default image survey - if len(input.platforms) == 0 { - cfgFound := false - cfgLocations := configLocations() - for _, v := range cfgLocations { - _, err := os.Stat(v) - if os.IsExist(err) { - cfgFound = true - } - } - if !cfgFound && len(cfgLocations) > 0 { - // The first config location refers to the global config folder one - if err := defaultImageSurvey(cfgLocations[0]); err != nil { - log.Fatal(err) - } - input.platforms = readArgsFile(cfgLocations[0], true) - } - } - deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`." - if input.privileged { - log.Warnf(deprecationWarning, "privileged", "--privileged") - } - if len(input.usernsMode) > 0 { - log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode)) - } - if len(input.containerCapAdd) > 0 { - log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd)) - } - if len(input.containerCapDrop) > 0 { - log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop)) - } - - // run the plan - config := &runner.Config{ - Actor: input.actor, - EventName: eventName, - EventPath: input.EventPath(), - DefaultBranch: defaultbranch, - ForcePull: !input.actionOfflineMode && input.forcePull, - ForceRebuild: input.forceRebuild, - ReuseContainers: input.reuseContainers, - Workdir: input.Workdir(), - ActionCacheDir: input.actionCachePath, - ActionOfflineMode: input.actionOfflineMode, - BindWorkdir: input.bindWorkdir, - LogOutput: !input.noOutput, - JSONLogger: input.jsonLogger, - LogPrefixJobID: input.logPrefixJobID, - Env: envs, - Secrets: secrets, - Vars: vars, - Inputs: inputs, - Token: secrets["GITHUB_TOKEN"], - InsecureSecrets: input.insecureSecrets, - Platforms: input.newPlatforms(), - Privileged: input.privileged, - UsernsMode: input.usernsMode, - ContainerArchitecture: input.containerArchitecture, - ContainerDaemonSocket: input.containerDaemonSocket, - ContainerOptions: input.containerOptions, - UseGitIgnore: input.useGitIgnore, - GitHubInstance: input.githubInstance, - ContainerCapAdd: input.containerCapAdd, - ContainerCapDrop: input.containerCapDrop, - AutoRemove: input.autoRemove, - ArtifactServerPath: input.artifactServerPath, - ArtifactServerAddr: input.artifactServerAddr, - ArtifactServerPort: input.artifactServerPort, - NoSkipCheckout: input.noSkipCheckout, - RemoteName: input.remoteName, - ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom, - ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom, - Matrix: matrixes, - ContainerNetworkMode: docker_container.NetworkMode(input.networkName), - } - if input.useNewActionCache || len(input.localRepository) > 0 { - if input.actionOfflineMode { - config.ActionCache = &runner.GoGitActionCacheOfflineMode{ - Parent: runner.GoGitActionCache{ - Path: config.ActionCacheDir, - }, - } - } else { - config.ActionCache = &runner.GoGitActionCache{ - Path: config.ActionCacheDir, - } - } - if len(input.localRepository) > 0 { - localRepositories := map[string]string{} - for _, l := range input.localRepository { - k, v, _ := strings.Cut(l, "=") - localRepositories[k] = v - } - config.ActionCache = &runner.LocalRepositoryCache{ - Parent: config.ActionCache, - LocalRepositories: localRepositories, - CacheDirCache: map[string]string{}, - } - } - } - r, err := runner.New(config) - if err != nil { - return err - } - - cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort) - - const cacheURLKey = "ACTIONS_CACHE_URL" - var cacheHandler *artifactcache.Handler - if !input.noCacheServer && envs[cacheURLKey] == "" { - var err error - cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerAddr, input.cacheServerPort, input.secret, common.Logger(ctx)) - if err != nil { - return err - } - envs[cacheURLKey] = cacheHandler.ExternalURL() + "/" - } - - ctx = common.WithDryrun(ctx, input.dryrun) - if watch, err := cmd.Flags().GetBool("watch"); err != nil { - return err - } else if watch { - err = watchAndRun(ctx, r.NewPlanExecutor(plan)) - if err != nil { - return err - } - return plannerErr - } - - executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { - cancel() - _ = cacheHandler.Close() - return nil - }) - err = executor(ctx) - if err != nil { - return err - } - return plannerErr - } -} - -func defaultImageSurvey(actrc string) error { - var answer string - confirmation := &survey.Select{ - 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)", - Help: "If you want to know why act asks you that, please go to https://github.com/nektos/act/issues/107", - Default: "Medium", - Options: []string{"Large", "Medium", "Micro"}, - } - - err := survey.AskOne(confirmation, &answer) - if err != nil { - return err - } - - var option string - switch answer { - case "Large": - 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" - case "Medium": - 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" - case "Micro": - 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" - } - - f, err := os.Create(actrc) - if err != nil { - return err - } - - _, err = f.WriteString(option) - if err != nil { - _ = f.Close() - return err - } - - err = f.Close() - if err != nil { - return err - } - - return nil -} - -func watchAndRun(ctx context.Context, fn common.Executor) error { - dir, err := os.Getwd() - if err != nil { - return err - } - - ignoreFile := filepath.Join(dir, ".gitignore") - ignore := &gitignore.GitIgnore{} - if info, err := os.Stat(ignoreFile); err == nil && !info.IsDir() { - ignore, err = gitignore.CompileIgnoreFile(ignoreFile) - if err != nil { - return fmt.Errorf("compile %q: %w", ignoreFile, err) - } - } - - folderWatcher := fswatch.NewFolderWatcher( - dir, - true, - ignore.MatchesPath, - 2, // 2 seconds - ) - - folderWatcher.Start() - defer folderWatcher.Stop() - - // run once before watching - if err := fn(ctx); err != nil { - return err - } - - for folderWatcher.IsRunning() { - log.Debugf("Watching %s for changes", dir) - select { - case <-ctx.Done(): - return nil - case changes := <-folderWatcher.ChangeDetails(): - log.Debugf("%s", changes.String()) - if err := fn(ctx); err != nil { - return err - } - } - } - - return nil -} diff --git a/cmd/secrets.go b/cmd/secrets.go deleted file mode 100644 index 25126f1c..00000000 --- a/cmd/secrets.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - log "github.com/sirupsen/logrus" - "golang.org/x/term" -) - -type secrets map[string]string - -func newSecrets(secretList []string) secrets { - s := make(map[string]string) - for _, secretPair := range secretList { - secretPairParts := strings.SplitN(secretPair, "=", 2) - secretPairParts[0] = strings.ToUpper(secretPairParts[0]) - if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] { - log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0]) - } - if len(secretPairParts) == 2 { - s[secretPairParts[0]] = secretPairParts[1] - } else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" { - s[secretPairParts[0]] = env - } else { - fmt.Printf("Provide value for '%s': ", secretPairParts[0]) - val, err := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Println() - if err != nil { - log.Errorf("failed to read input: %v", err) - os.Exit(1) - } - s[secretPairParts[0]] = string(val) - } - } - return s -} - -func (s secrets) AsMap() map[string]string { - return s -} diff --git a/main.go b/main.go deleted file mode 100644 index 37b0fece..00000000 --- a/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "context" - _ "embed" - "os" - "os/signal" - "syscall" - - "github.com/nektos/act/cmd" -) - -//go:embed VERSION -var version string - -func main() { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - - // trap Ctrl+C and call cancel on the context - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - defer func() { - signal.Stop(c) - cancel() - }() - go func() { - select { - case <-c: - cancel() - case <-ctx.Done(): - } - }() - - // run the command - cmd.Execute(ctx, version) -}