| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/spf13/cobra" |
| 10 | "github.com/spf13/pflag" |
| 11 | |
| 12 | infralog "github.com/tenseleyFlow/shithub/internal/infra/log" |
| 13 | runnerpkg "github.com/tenseleyFlow/shithub/internal/runner" |
| 14 | runnerapi "github.com/tenseleyFlow/shithub/internal/runner/api" |
| 15 | runnerconfig "github.com/tenseleyFlow/shithub/internal/runner/config" |
| 16 | "github.com/tenseleyFlow/shithub/internal/runner/engine" |
| 17 | "github.com/tenseleyFlow/shithub/internal/runner/workspace" |
| 18 | ) |
| 19 | |
| 20 | var runConfigPath string |
| 21 | |
| 22 | var runCmd = &cobra.Command{ |
| 23 | Use: "run", |
| 24 | Short: "Claim and execute shithub Actions jobs", |
| 25 | RunE: func(cmd *cobra.Command, _ []string) error { |
| 26 | cfg, err := runnerconfig.Load(runnerconfig.LoadOptions{ |
| 27 | ConfigPath: runConfigPath, |
| 28 | Overrides: flagOverrides(cmd), |
| 29 | }) |
| 30 | if err != nil { |
| 31 | return err |
| 32 | } |
| 33 | logger := infralog.New(infralog.Options{ |
| 34 | Level: cfg.Log.Level, |
| 35 | Format: cfg.Log.Format, |
| 36 | Writer: os.Stderr, |
| 37 | }).With("component", "runner") |
| 38 | |
| 39 | workspaces := workspace.New(cfg.Runner.WorkspaceRoot) |
| 40 | removed, err := workspaces.Sweep(cfg.Runner.WorkspaceTTL, time.Now().UTC()) |
| 41 | if err != nil { |
| 42 | return err |
| 43 | } |
| 44 | if removed > 0 { |
| 45 | logger.InfoContext(cmd.Context(), "swept stale workspaces", "count", removed) |
| 46 | } |
| 47 | |
| 48 | client, err := runnerapi.New(runnerapi.Config{ |
| 49 | BaseURL: cfg.Server.BaseURL, |
| 50 | RunnerToken: cfg.Runner.Token, |
| 51 | }) |
| 52 | if err != nil { |
| 53 | return err |
| 54 | } |
| 55 | execEngine := engine.NewDocker(engine.DockerConfig{ |
| 56 | Binary: cfg.Engine.Kind, |
| 57 | DefaultImage: cfg.Engine.DefaultImage, |
| 58 | Network: cfg.Engine.Network, |
| 59 | Memory: cfg.Engine.Memory, |
| 60 | CPUs: cfg.Engine.CPUs, |
| 61 | Stdout: os.Stdout, |
| 62 | Stderr: os.Stderr, |
| 63 | }) |
| 64 | r := runnerpkg.New(runnerpkg.Options{ |
| 65 | API: client, |
| 66 | Engine: execEngine, |
| 67 | Workspaces: workspaces, |
| 68 | Logger: logger, |
| 69 | Labels: cfg.Runner.Labels, |
| 70 | Capacity: cfg.Runner.Capacity, |
| 71 | PollInterval: cfg.Runner.PollInterval, |
| 72 | DefaultImage: cfg.Engine.DefaultImage, |
| 73 | Clock: func() time.Time { return time.Now().UTC() }, |
| 74 | }) |
| 75 | return r.Run(cmd.Context()) |
| 76 | }, |
| 77 | } |
| 78 | |
| 79 | func init() { |
| 80 | runCmd.Flags().StringVar(&runConfigPath, "config", "", "Path to runner config file") |
| 81 | runCmd.Flags().String("server-url", "", "shithub base URL") |
| 82 | runCmd.Flags().String("token", "", "Runner registration token") |
| 83 | runCmd.Flags().String("labels", "", "Comma-separated runner labels") |
| 84 | runCmd.Flags().Int("capacity", 0, "Maximum concurrent jobs this runner advertises") |
| 85 | runCmd.Flags().Duration("poll-interval", 0, "Idle heartbeat interval") |
| 86 | runCmd.Flags().String("workspace-root", "", "Workspace root directory") |
| 87 | runCmd.Flags().Duration("workspace-ttl", 0, "Startup sweep TTL for stale workspaces") |
| 88 | runCmd.Flags().String("engine", "", "Execution engine: docker or podman") |
| 89 | runCmd.Flags().String("image", "", "Default container image") |
| 90 | runCmd.Flags().String("network", "", "Container network") |
| 91 | runCmd.Flags().String("memory", "", "Container memory limit") |
| 92 | runCmd.Flags().String("cpus", "", "Container CPU limit") |
| 93 | runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error") |
| 94 | runCmd.Flags().String("log-format", "", "Log format: text or json") |
| 95 | } |
| 96 | |
| 97 | func flagOverrides(cmd *cobra.Command) map[string]string { |
| 98 | keys := map[string]string{ |
| 99 | "server-url": "server.base_url", |
| 100 | "token": "runner.token", |
| 101 | "labels": "runner.labels", |
| 102 | "capacity": "runner.capacity", |
| 103 | "poll-interval": "runner.poll_interval", |
| 104 | "workspace-root": "runner.workspace_root", |
| 105 | "workspace-ttl": "runner.workspace_ttl", |
| 106 | "engine": "engine.kind", |
| 107 | "image": "engine.default_image", |
| 108 | "network": "engine.network", |
| 109 | "memory": "engine.memory", |
| 110 | "cpus": "engine.cpus", |
| 111 | "log-level": "log.level", |
| 112 | "log-format": "log.format", |
| 113 | } |
| 114 | out := make(map[string]string) |
| 115 | cmd.Flags().Visit(func(f *pflag.Flag) { |
| 116 | if key, ok := keys[f.Name]; ok { |
| 117 | out[key] = f.Value.String() |
| 118 | } |
| 119 | }) |
| 120 | return out |
| 121 | } |
| 122 |