A tool for managing macOS virtual machines as GitHub Actions self-hosted runners using Tart on Apple Silicon.
- VM Management: Build, configure, and manage Tart VMs for GitHub Actions runners
- Runner Daemon: Automatically start and manage GitHub Actions runners
- Log Monitoring: Send VM logs to PostHog for centralized monitoring across multiple machines
- Interactive TUI: User-friendly terminal interface for all operations
- Headless Mode: Run in background via LaunchAgent/LaunchDaemon
- Automatic Updates: Install verified GitHub releases silently through a dedicated root updater
- macOS on Apple Silicon (M1/M2/M3)
- Homebrew
- Required tools (can be installed via setup command):
tart- VM managementsshpass- SSH automationwget- File downloadspacker- VM image building
Download the notarized rvmm_macOS_arm64.pkg installer from the GitHub Releases page and install it. The binary is installed to /usr/local/bin/rvmm.
The package also installs lab.rxtech.rvmm.updater, a narrowly scoped root LaunchDaemon. The initial package installation requires administrator approval. After that, the updater checks for a new published GitHub release at boot and every six hours, verifies the SHA-256 checksum, Developer ID Installer signature, Apple team, and notarization, then installs it without prompting.
Select Update rvmm in the TUI to request an immediate background check. Installing an update does not terminate the currently running RVMM process or active VM work; the new binary is used on the next safe service start.
The CLI lives in the cli/ directory:
git clone <repository-url>
cd macos-github-action-vm/cli
make build # outputs cli/bin/rvmmCreate a rvmm.yaml configuration file (see cli/assets/config.yaml.example for a template):
github:
api_token: "ghp_xxxxxxxxxxxxxxxxxxxx"
registration_endpoint: "https://api.github.com/orgs/YOUR_ORG/actions/runners/registration-token"
runner_url: "https://github.com/YOUR_ORG"
runner_name: "runner"
runner_labels:
- self-hosted
- arm64
- macOS
runner_group: ""
vm:
username: "admin"
password: "admin"
registry:
url: ""
image_name: "runner:latest"
username: ""
password: ""
options:
truncate_size: ""
log_file: "runner.log"
max_concurrent_runners: 1
shutdown_flag_file: ".shutdown"
working_directory: "/Users/admin/vm"
daemon:
label: "com.mirego.ekiden"
plist_path: "/Users/admin/Library/LaunchAgents/com.mirego.ekiden.plist"
user: "admin"
posthog:
enabled: false
api_key: "phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
host: "https://app.posthog.com"
machine_label: "machine-1"Run without arguments to start the interactive terminal UI:
./rvmmThe TUI provides menus for:
- Setup: Install required dependencies
- Build: Build VM image from an external Packer template
- Config: Edit configuration
- Run: Start runners interactively
- Images: List local VM images
- Daemon: Install/manage runner daemon
- Monitor Daemon: Install/manage log monitoring daemon
- View Logs: Tail log files
VM image templates are not bundled with the CLI — they are provided externally. When you select "Build VM image" in the TUI, you are prompted for the path to a Packer template (.pkr.hcl). The CLI runs packer init and packer build in the template's directory, so any supporting files (setup scripts, etc.) should live next to the template.
An example template is provided in guest/runner.pkr.hcl:
rvmm # Open TUI
# Select "Build VM image" and enter e.g. /path/to/guest/runner.pkr.hclYou can maintain your own templates in a separate repository and point the CLI at them.
Start the GitHub Actions runner in foreground:
./rvmm run -config rvmm.yamlStart log monitoring to send logs to PostHog:
./rvmm monitor -config rvmm.yamlThis monitors /Users/qiweili/rvmm/stdout and /Users/qiweili/rvmm/stderr and sends each new log line to PostHog with the configured machine label.
Install as a LaunchAgent/LaunchDaemon to run automatically:
Via TUI:
- Start
./rvmm - Select "Daemon" → "Install daemon"
Via Command Line:
- The daemon can be controlled using standard
launchctlcommands after installation
Install log monitoring as a background service:
Via TUI:
- Start
./rvmm - Select "Monitor Daemon" → "Install monitor daemon"
Requirements:
- PostHog must be enabled in
rvmm.yaml - Valid PostHog API key and machine label configured
The monitor daemon will:
- Start automatically on user login
- Continuously monitor log files
- Send new log lines to PostHog with machine label for differentiation
- Restart automatically if it crashes
Check Status:
launchctl print gui/$(id -u)/com.mirego.ekiden.monitorView Monitor Logs:
tail -f /Users/qiweili/rvmm/monitor_stderr.logThe log monitoring feature sends VM logs to PostHog for centralized analysis across multiple machines.
-
Get PostHog API Key:
- Sign up at posthog.com or use your self-hosted instance
- Get your Project API Key from Settings → Project
-
Configure in
rvmm.yaml:posthog: enabled: true api_key: "phc_your_actual_api_key" host: "https://app.posthog.com" machine_label: "mac-studio-1" # Unique label for this machine
-
Install Monitor Daemon:
./rvmm # Open TUI # Select "Monitor Daemon" → "Install monitor daemon"
-
Files monitored:
/Users/qiweili/rvmm/stdout/Users/qiweili/rvmm/stderr
-
Each log line is sent as a PostHog event with properties:
machine_label: Your configured label (e.g., "mac-studio-1")log_type: "stdout" or "stderr"log_line: The actual log contenttimestamp: When the log was captured
In PostHog, you can:
- Filter events by
machine_labelto see logs from specific machines - Filter by
log_typeto see only stdout or stderr - Search
log_linecontent for specific errors or patterns - Create dashboards to monitor error rates across machines
- cli: The rvmm CLI (Go module)
- cli/internal/config: Configuration management with Viper
- cli/internal/runner: GitHub Actions runner logic and VM management
- cli/internal/daemon: LaunchAgent/LaunchDaemon installation and management
- cli/internal/monitor: Log file monitoring with tail-follow logic
- cli/internal/posthog: PostHog API client for log event capture
- cli/internal/tui: Bubble Tea terminal UI
- cli/internal/updater: GitHub release discovery, verified downloads, updater state, and package installation
- cli/assets: Embedded plist template and example config
- cli/scripts: Build, signing, and notarization scripts for releases
- guest: Example external Packer template for building the runner VM image
Releases are automated via GitHub Actions:
- The "Create a new release" workflow (
create-release.yaml) runs semantic-release to tag and create a GitHub release. - The "Release rvmm CLI" workflow (
release-cli.yaml) triggers on the release event: it stamps the tag into the binary, signs it, verifies its Mach-O UUID, packages and notarizes it, and uploadsrvmm_macOS_arm64.pkgplus its SHA-256 checksum.
After installation, daemons create the following files:
- Plist: As configured in
daemon.plist_path - Logs:
${working_directory}/stdout,${working_directory}/stderr
- Plist:
${daemon.plist_path}with.monitorsuffix - Logs:
${working_directory}/monitor_stdout.log,${working_directory}/monitor_stderr.log
- Plist:
/Library/LaunchDaemons/lab.rxtech.rvmm.updater.plist - Status:
/Library/Application Support/RVMM/Updater/status.json - Requests:
/Library/Application Support/RVMM/Updater/requests/ - Logs:
/Library/Logs/rvmm-updater.log
The updater is intentionally separate from the runner daemon. Only verified package installation runs as root; Tart and GitHub Actions runner operations continue under daemon.user.
Check daemon status:
launchctl print gui/$(id -u)/com.mirego.ekidenView logs:
tail -f /Users/qiweili/rvmm/stderrCheck if monitor is running:
launchctl print gui/$(id -u)/com.mirego.ekiden.monitorView monitor logs:
tail -f /Users/qiweili/rvmm/monitor_stderr.logCommon issues:
- PostHog not enabled: Ensure
posthog.enabled: truein config - Missing API key: Add valid PostHog API key to config
- Invalid machine_label: Set a unique label for each machine
- Log files not found: Ensure runner daemon is running and creating logs
Via TUI:
- Start
./rvmm - Select "Daemon" → "Uninstall daemon" (for runner)
- Select "Monitor Daemon" → "Uninstall monitor daemon" (for monitoring)
Manual Cleanup:
# Uninstall runner daemon
launchctl bootout gui/$(id -u)/com.mirego.ekiden
rm ~/Library/LaunchAgents/com.mirego.ekiden.plist
# Uninstall monitor daemon
launchctl bootout gui/$(id -u)/com.mirego.ekiden.monitor
rm ~/Library/LaunchAgents/com.mirego.ekiden.monitor.plist[Your License Here]
[Contributing Guidelines Here]