package cmd import ( "context" "fmt" "os" "path/filepath" "strings" "gitea.com/gitea/act_runner/cmd/config" "github.com/joho/godotenv" "github.com/mattn/go-isatty" "github.com/nektos/act/pkg/artifacts" "github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/runner" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) const version = "0.1" type Input struct { actor string // workdir string // workflowsPath string // autodetectEvent bool // eventPath string reuseContainers bool bindWorkdir bool // secrets []string // envs []string // platforms []string // dryrun bool forcePull bool forceRebuild bool // noOutput bool // envfile string // secretfile string insecureSecrets bool // defaultBranch string privileged bool usernsMode string containerArchitecture string containerDaemonSocket string // noWorkflowRecurse bool useGitIgnore bool forgeInstance string containerCapAdd []string containerCapDrop []string autoRemove bool artifactServerPath string artifactServerPort string jsonLogger bool noSkipCheckout bool // remoteName string } func (i *Input) newPlatforms() map[string]string { return map[string]string{ "ubuntu-latest": "node:16-buster-slim", "ubuntu-20.04": "node:16-buster-slim", "ubuntu-18.04": "node:16-buster-slim", } } // helper function cfgures the logging. func initLogging(cfg config.Config) { isTerm := isatty.IsTerminal(os.Stdout.Fd()) switch strings.ToLower(cfg.Logging.Level) { case "panic": zerolog.SetGlobalLevel(zerolog.PanicLevel) case "fatal": zerolog.SetGlobalLevel(zerolog.FatalLevel) case "error": zerolog.SetGlobalLevel(zerolog.ErrorLevel) case "warn": zerolog.SetGlobalLevel(zerolog.WarnLevel) case "info": zerolog.SetGlobalLevel(zerolog.InfoLevel) case "debug": zerolog.SetGlobalLevel(zerolog.DebugLevel) default: zerolog.SetGlobalLevel(zerolog.InfoLevel) } if cfg.Logging.Pretty || isTerm { log.Logger = log.Output( zerolog.ConsoleWriter{ Out: os.Stderr, NoColor: cfg.Logging.NoColor || !isTerm, }, ) } } func Execute(ctx context.Context) { var envfile string input := Input{ reuseContainers: true, forgeInstance: "gitea.com", } rootCmd := &cobra.Command{ Use: "act [event name to run]\nIf no event name passed, will default to \"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: runCommand(ctx, &input), Version: version, SilenceUsage: true, } rootCmd.AddCommand(&cobra.Command{ Aliases: []string{"daemon"}, Use: "daemon [event name to run]\nIf no event name passed, will default to \"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: runDaemon(ctx, &input), }) rootCmd.Flags().BoolP("run", "r", false, "run workflows") rootCmd.Flags().StringP("job", "j", "", "run job") rootCmd.PersistentFlags().StringVarP(&input.forgeInstance, "forge-instance", "", "github.com", "Forge instance to use.") rootCmd.PersistentFlags().StringVarP(&envfile, "env-file", "", ".env", "Read in a file of environment variables.") _ = godotenv.Load(envfile) cfg, err := config.Environ() if err != nil { log.Fatal(). Err(err). Msg("invalid cfguration") } initLogging(cfg) if err := rootCmd.Execute(); err != nil { os.Exit(1) } } // getWorkflowsPath return the workflows directory, it will try .gitea first and then fallback to .github func getWorkflowsPath() (string, error) { dir, err := os.Getwd() if err != nil { return "", err } p := filepath.Join(dir, ".gitea/workflows") _, err = os.Stat(p) if err != nil { if !os.IsNotExist(err) { return "", err } return filepath.Join(dir, ".github/workflows"), nil } return p, nil } func runTask(ctx context.Context, input *Input, jobID string) error { workflowsPath, err := getWorkflowsPath() if err != nil { return err } planner, err := model.NewWorkflowPlanner(workflowsPath, false) if err != nil { return err } var eventName string events := planner.GetEvents() if len(events) > 0 { // set default event type to first event // this way user dont have to specify the event. log.Debug().Msgf("Using detected workflow event: %s", events[0]) eventName = events[0] } else { if plan := planner.PlanEvent("push"); plan != nil { eventName = "push" } } // build the plan for this run var plan *model.Plan if jobID != "" { log.Debug().Msgf("Planning job: %s", jobID) plan = planner.PlanJob(jobID) } else { log.Debug().Msgf("Planning event: %s", eventName) plan = planner.PlanEvent(eventName) } curDir, err := os.Getwd() if err != nil { return err } // run the plan config := &runner.Config{ Actor: input.actor, EventName: eventName, EventPath: "", DefaultBranch: "", ForcePull: input.forcePull, ForceRebuild: input.forceRebuild, ReuseContainers: input.reuseContainers, Workdir: curDir, BindWorkdir: input.bindWorkdir, LogOutput: true, JSONLogger: input.jsonLogger, // Env: envs, // Secrets: secrets, InsecureSecrets: input.insecureSecrets, Platforms: input.newPlatforms(), Privileged: input.privileged, UsernsMode: input.usernsMode, ContainerArchitecture: input.containerArchitecture, ContainerDaemonSocket: input.containerDaemonSocket, UseGitIgnore: input.useGitIgnore, GitHubInstance: input.forgeInstance, ContainerCapAdd: input.containerCapAdd, ContainerCapDrop: input.containerCapDrop, AutoRemove: input.autoRemove, ArtifactServerPath: input.artifactServerPath, ArtifactServerPort: input.artifactServerPort, NoSkipCheckout: input.noSkipCheckout, // RemoteName: input.remoteName, } r, err := runner.New(config) if err != nil { return fmt.Errorf("New config failed: %v", err) } cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort) executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { cancel() return nil }) return executor(ctx) } func runCommand(ctx context.Context, input *Input) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { jobID, err := cmd.Flags().GetString("job") if err != nil { return err } return runTask(ctx, input, jobID) } }