From f3b68e80951dc5b2c3a4e0bb02b322c9b2a10516 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:30:07 +0800 Subject: [PATCH 1/2] fix: cli build error --- .github/workflows/release-cli.yaml | 6 +- .github/workflows/upload-docs.yaml | 29 +++++ docs/architecture/overview.md | 63 ++++++++++ docs/code/go.md | 129 ++++++++++++++++++++ docs/operations/configuration.md | 96 +++++++++++++++ scripts/upload_docs.py | 187 +++++++++++++++++++++++++++++ 6 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/upload-docs.yaml create mode 100644 docs/architecture/overview.md create mode 100644 docs/code/go.md create mode 100644 docs/operations/configuration.md create mode 100644 scripts/upload_docs.py diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml index b63f168..90453cc 100644 --- a/.github/workflows/release-cli.yaml +++ b/.github/workflows/release-cli.yaml @@ -23,8 +23,9 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version-file: cli/go.mod + go-version: '1.26.x' check-latest: true + cache-dependency-path: cli/go.sum - name: Install dependencies run: go mod download @@ -43,6 +44,9 @@ jobs: env: SIGNING_CERTIFICATE_NAME: ${{ secrets.SIGNING_CERTIFICATE_NAME }} + - name: Verify Mach-O UUID + run: otool -l bin/rvmm | grep -q 'cmd LC_UUID' + - name: Import installer signing certificate uses: apple-actions/import-codesign-certs@v3 with: diff --git a/.github/workflows/upload-docs.yaml b/.github/workflows/upload-docs.yaml new file mode 100644 index 0000000..de8a4c1 --- /dev/null +++ b/.github/workflows/upload-docs.yaml @@ -0,0 +1,29 @@ +name: Upload docs to autopilot + +on: + push: + branches: ["main"] + paths: + - "docs/**" + - "scripts/upload_docs.py" + - ".github/workflows/upload-docs.yaml" + workflow_dispatch: + +concurrency: + group: upload-docs + cancel-in-progress: false + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Upload docs + env: + DOCS_ENDPOINT: https://autopilot.rxlab.app + DOCS_REPOSITORY_ID: rxtech-lab/macos-github-action-vm + DOCS_UPLOAD_TOKEN: ${{ secrets.DOCS_UPLOAD_TOKEN }} + run: python scripts/upload_docs.py diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..46a048e --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,63 @@ +--- +slug: architecture/overview +title: RVMM Architecture Overview +description: Components and runtime flow of the macOS GitHub Actions runner VM manager +--- + +# RVMM architecture overview + +RVMM is a Go command-line application for running ephemeral GitHub Actions self-hosted runners inside Tart virtual machines on Apple Silicon Macs. It combines host setup, image management, runner registration, bounded VM concurrency, launchd integration, and optional PostHog log forwarding. + +## Repository layout + +| Area | Responsibility | +| --- | --- | +| `cli/main.go` | Selects the interactive TUI, headless runner, or headless monitor entry point. | +| `cli/internal/config` | Loads `rvmm.yaml`, applies defaults, and validates required settings. | +| `cli/internal/runner` | Obtains GitHub registration tokens and manages Tart VM, SSH, and ephemeral runner lifecycles. | +| `cli/internal/tui` | Implements the Bubble Tea menus for setup, configuration, image operations, runners, daemons, and logs. | +| `cli/internal/daemon` | Installs, removes, and inspects runner and monitor launchd jobs. | +| `cli/internal/monitor` | Tails runner stdout and stderr files. | +| `cli/internal/posthog` | Sends new log lines to PostHog capture endpoints. | +| `cli/internal/setup` | Installs or validates host dependencies and creates a sample configuration. | +| `cli/assets` | Embeds the launchd plist template and example configuration in the binary. | +| `guest/runner.pkr.hcl` | Provides an example Packer template for a Tart runner image. | +| `.github/workflows` | Creates releases and builds, signs, notarizes, and publishes the macOS installer. | + +## Runtime modes + +Running `rvmm` without arguments opens the interactive TUI. The TUI calls the same config, setup, runner, daemon, and image-management packages used by headless operation. + +`rvmm run -config rvmm.yaml` starts the runner loop. `rvmm monitor -config rvmm.yaml` starts two log tailers for the configured working directory and requires `posthog.enabled: true`. + +## Runner lifecycle + +1. Load and validate configuration. +2. Verify that `tart`, `sshpass`, `wget`, and `packer` are available. +3. Log in to the OCI registry when credentials are configured. +4. Reuse a local Tart image or pull the configured image from the registry. +5. Create `options.max_concurrent_runners` worker slots. +6. For each slot, request a short-lived registration token from GitHub. +7. Clone the base image to an instance named `_` and boot it without graphics. +8. Wait for the guest IP and SSH, then configure an ephemeral GitHub Actions runner in the guest. +9. Run one Actions job, stop the VM, and delete the instance during cleanup. +10. Return the slot to the pool and repeat until the context is cancelled or the shutdown flag exists. + +Each worker owns its own `VMManager`, while the GitHub client is shared. The base image is initialized once before workers begin. + +## VM image construction + +The example Packer template uses the Tart plugin and starts from a Cirrus Labs macOS image. It downloads the latest arm64 GitHub Actions runner into the guest, installs Apple certificate authorities, selects Xcode, accepts its license, completes first-launch setup, and downloads available platforms and simulator runtimes. + +The CLI accepts external `.pkr.hcl` templates, runs `packer init`, and then runs `packer build` from the selected template's directory. Supporting provisioner files therefore stay beside the template. + +## Background services and observability + +The runner can be installed as a system LaunchDaemon or user LaunchAgent depending on `daemon.plist_path`. Its stdout and stderr are written under `options.working_directory`. + +The optional monitor runs as a separate LaunchAgent. It polls both files, starts at their current end, detects truncation, and forwards subsequent non-empty lines as `mac_ci_log_line` PostHog events identified by `posthog.machine_label`. + +## Release flow + +The manual semantic-release workflow creates a GitHub release from `main`. The release workflow runs on the self-hosted macOS ARM64 runner, executes Go tests, builds and signs `rvmm`, creates and notarizes a macOS installer package for release events, and uploads the package to the GitHub release. + diff --git a/docs/code/go.md b/docs/code/go.md new file mode 100644 index 0000000..0843577 --- /dev/null +++ b/docs/code/go.md @@ -0,0 +1,129 @@ +--- +slug: code/go-packages +title: Go Package Reference +description: Go package index for the RVMM command-line application +--- + +# Go package reference + +This index is derived from `go doc` for module `github.com/rxtech-lab/rvmm` using Go 1.22. Run `go doc -all ` from `cli/` for full declarations and field documentation. + +## `github.com/rxtech-lab/rvmm` + +The executable package selects the Bubble Tea TUI by default. It also exposes the `run` and `monitor` command modes through the process entry point. + +## `assets` + +Embedded build assets: + +```text +var ConfigExample []byte +var EkidenPlist []byte +``` + +## `internal/config` + +Loads, defaults, and validates the YAML configuration. + +```text +type Config struct { ... } + func Load(configPath string) (*Config, error) +type DaemonConfig struct { ... } +type GitHubConfig struct { ... } +type OptionsConfig struct { ... } +type PostHogConfig struct { ... } +type RegistryConfig struct { ... } +type VMConfig struct { ... } +func (c *Config) Validate() error +``` + +## `internal/daemon` + +Manages launchd jobs for the runner and log monitor. + +```text +func Install(log *zap.Logger, cfg *config.Config, configPath string, out io.Writer) error +func InstallMonitor(log *zap.Logger, cfg *config.Config, configPath string, out io.Writer) error +func IsRunning(cfg *config.Config) (bool, error) +func Status(log *zap.Logger, cfg *config.Config, out io.Writer) error +func StatusMonitor(log *zap.Logger, cfg *config.Config, out io.Writer) error +func Uninstall(log *zap.Logger, cfg *config.Config, out io.Writer) error +func UninstallMonitor(log *zap.Logger, cfg *config.Config, out io.Writer) error +type PlistData struct { ... } +``` + +## `internal/monitor` + +Polls a log file and sends new lines to PostHog. + +```text +type LogTailer struct { ... } + func NewLogTailer(filePath string, logType string, posthog *posthog.Client, log *zap.Logger) *LogTailer + func (t *LogTailer) Start(ctx context.Context) error +``` + +## `internal/posthog` + +Builds and sends single or batched PostHog capture requests. + +```text +type CaptureRequest struct { ... } +type Client struct { ... } + func NewClient(cfg *config.PostHogConfig, log *zap.Logger) *Client + func (c *Client) CaptureLogEvent(logType string, logLine string) error + func (c *Client) CaptureLogEventBatch(logType string, logLines []string) error +type Event struct { ... } +``` + +## `internal/runner` + +Coordinates registration tokens, Tart instances, SSH, and ephemeral runner workers. + +```text +func Run(ctx context.Context, log *zap.Logger, cfg *config.Config) error +type GitHubClient struct { ... } + func NewGitHubClient(cfg *config.Config, log *zap.Logger) *GitHubClient + func (g *GitHubClient) GetRegistrationToken() (string, error) +type RegistrationTokenResponse struct { ... } +type SSHClient struct { ... } + func NewSSHClient(cfg *config.Config, log *zap.Logger) *SSHClient + func (s *SSHClient) ConfigureRunner(ctx context.Context, ip, token, runnerName string) error + func (s *SSHClient) Execute(ctx context.Context, ip, command string, showOutput bool) error + func (s *SSHClient) ExecuteWithOutput(ctx context.Context, ip, command string) (string, error) + func (s *SSHClient) RunRunner(ctx context.Context, ip string) error + func (s *SSHClient) WaitForSSH(ctx context.Context, ip string) error +type VMManager struct { ... } + func NewVMManager(cfg *config.Config, log *zap.Logger) *VMManager + func (v *VMManager) Cleanup(ctx context.Context, instanceName string) + func (v *VMManager) Clone(ctx context.Context, instanceName string) error + func (v *VMManager) Delete(ctx context.Context, instanceName string) error + func (v *VMManager) GetCachePath() string + func (v *VMManager) GetRegistryPath() string + func (v *VMManager) ImageExists(ctx context.Context) (bool, error) + func (v *VMManager) Login(ctx context.Context) error + func (v *VMManager) PullImage(ctx context.Context) error + func (v *VMManager) Start(ctx context.Context, instanceName string) (*exec.Cmd, error) + func (v *VMManager) Stop(ctx context.Context, instanceName string) error + func (v *VMManager) WaitForIP(ctx context.Context, instanceName string) (string, error) +``` + +## `internal/setup` + +Installs and validates host dependencies and creates the sample configuration. + +```text +var RequiredPackages = []string { ... } +var RequiredTools = []string { ... } +func CheckDependencies() error +func Run(log *zap.Logger) error +func RunWithIO(log *zap.Logger, stdout, stderr io.Writer, stdin io.Reader) error +``` + +## `internal/tui` + +Implements the Bubble Tea interface and delegates work to the setup, runner, daemon, and image-management packages. + +```text +func Run() +``` + diff --git a/docs/operations/configuration.md b/docs/operations/configuration.md new file mode 100644 index 0000000..6e0f3f6 --- /dev/null +++ b/docs/operations/configuration.md @@ -0,0 +1,96 @@ +--- +slug: operations/configuration +title: RVMM Configuration and Operations +description: Configure, run, monitor, and release RVMM +--- + +# RVMM configuration and operations + +RVMM reads YAML from the path passed with `-config`. Without an explicit path it searches for `rvmm.yaml` in the current directory, `$HOME/.rvmm`, and `/etc/rvmm`. The interactive setup flow writes an example `rvmm.yaml` in the current directory if one does not already exist. + +## Host requirements + +RVMM is intended for macOS on Apple Silicon with hardware virtualization enabled. Runtime and image-building operations use: + +- `tart` for VM images and instances +- `sshpass` for guest automation +- `wget` for downloads +- `packer` from `hashicorp/tap` for image templates + +The TUI Setup action can install Homebrew and these packages. + +## Configuration sections + +### `github` + +| Field | Meaning | +| --- | --- | +| `api_token` | GitHub token used to request runner registration tokens. | +| `registration_endpoint` | Organization or repository runner registration-token endpoint. | +| `runner_url` | Organization or repository URL passed to the guest runner configuration. | +| `runner_name` | Base runner name; the worker slot is appended at runtime. | +| `runner_labels` | Labels registered on each ephemeral runner. | +| `runner_group` | Optional organization or enterprise runner group. | + +`api_token`, `registration_endpoint`, and `runner_url` are required. + +### `vm` + +`username` and `password` are the guest SSH credentials and are required. `display` controls the Tart display configuration applied before boot and defaults to `3840x2160`. + +### `registry` + +`image_name` is required and can name a local Tart image or an OCI image. Set `url`, `username`, and `password` when RVMM must authenticate and pull from a registry. If `image_name` already starts with the configured registry URL, RVMM avoids adding the prefix twice. + +### `options` + +| Field | Default | Meaning | +| --- | --- | --- | +| `truncate_size` | empty | Optional target size used when resizing a pulled image. | +| `log_file` | `runner.log` | TUI log path. | +| `max_concurrent_runners` | `1` | Number of VM worker slots; must be at least one. | +| `shutdown_flag_file` | `.shutdown` | Stops new work and waits for active workers when this file exists. | +| `working_directory` | `/Users/admin/vm` | Runner logs and launchd working directory. | + +### `daemon` + +`label` identifies the launchd job, `plist_path` selects a system LaunchDaemon or user LaunchAgent location, and `user` is written into the generated plist. Installing under `/Library/LaunchDaemons` generally requires elevated filesystem permissions. + +### `posthog` + +Set `enabled: true`, then provide `api_key`, `host`, and a unique `machine_label` to forward runner logs. These values are validated only when monitoring is enabled. + +## Common commands + +```bash +# Build and test the CLI +cd cli +make build +make test + +# Run interactively +./bin/rvmm + +# Run ephemeral workers in the foreground +./bin/rvmm run -config /path/to/rvmm.yaml + +# Forward new runner logs to PostHog +./bin/rvmm monitor -config /path/to/rvmm.yaml + +# Build the example guest image +cd ../guest +packer init runner.pkr.hcl +packer build runner.pkr.hcl +``` + +## Operational checks + +- Confirm the image is visible with `tart list` before starting workers. +- Confirm the guest credentials match the Packer image and that SSH is reachable. +- Confirm the GitHub token can call the configured registration endpoint. +- Check the configured launchd domain with `launchctl print system/