Skip to content

diranged/graftery

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Graftery

Graftery

Ephemeral macOS VMs for GitHub Actions — powered by Tart

Platform Protocol Virtualization License


Why Graftery?

If you run iOS, macOS, or Apple-platform CI on GitHub Actions, you need macOS runners. GitHub's hosted runners work, but they're expensive and you can't customize the image. Self-hosted runners on bare metal are fast and cheap — but managing them is painful: stale state bleeds between jobs, runner registration is manual, and there's no easy way to scale.

Graftery fixes this. It brings the same ephemeral, scale-to-zero model that Actions Runner Controller (ARC) provides on Kubernetes — but runs directly on a Mac. Every job gets a fresh VM clone. When the job finishes, the VM is destroyed. No state leaks, no drift, no cleanup scripts.

How it works

GitHub Actions                        Your Mac
─────────────                         ────────
  Job queued  ──── scaleset poll ────▶  Graftery sees demand
                                        │
                                        ├─ Clones base VM image
                                        ├─ Injects JIT runner config
                                        ├─ Boots ephemeral Tart VM
                                        ├─ Runner picks up the job
                                        └─ VM destroyed on completion

Graftery speaks the actions/scaleset protocol natively — the same wire protocol ARC uses. No custom API, no webhook glue.

Core capabilities

Clean room every job — Each job runs in a fresh VM clone. No state leaks between jobs, ever.

Scale to zero — No jobs? No VMs. Runners spin up on demand and tear down when done. Configure a warm pool (min_runners) for faster pickup.

Custom VM images — Drop shell scripts into bake.d/ and Graftery bakes them into a prepared image. Install Xcode, CocoaPods, Homebrew packages — whatever your builds need. Content-hashed so reprovisioning only happens when scripts change.

Pre/post job hooks — Native GitHub Actions runner hooks (ACTIONS_RUNNER_HOOK_JOB_STARTED / COMPLETED). They show up as collapsible sections in the Actions UI.

Orphan cleanup — On startup, Graftery finds and removes VMs left behind by crashes. Session conflicts with GitHub are retried automatically with exponential backoff.

Prometheus metrics — Host CPU/memory/disk, per-VM CPU/memory/uptime, job counters — all exposed via a /metrics endpoint. Includes Apple hypervisor (XPC) process tracking for accurate VM resource attribution.

Dry-run mode — Test your setup without GitHub or Tart. Simulates the full lifecycle with fake jobs so you can validate config, control socket, and UI integration end-to-end.

Two ways to run it

Graftery ships as both a macOS menu bar app and a standalone CLI. They share the same Go backend — the app wraps the CLI in a native Swift UI.

Best for Interactive use on a Mac with a display Headless servers, automation, launchd/systemd
Install Download DMG Download binary
Runner sets Multiple — manage unlimited independent configs One per process
Config 6-step setup wizard + tabbed editor with auto-save YAML file + CLI flags
Metrics Live time-series charts (CPU & memory), menu bar gauges Prometheus /metrics endpoint
Logs Built-in log viewer with search, level filtering, color Structured logs to stderr
Controls Menu bar start/stop per runner, enable/disable auto-start SIGINT/SIGTERM
Runs as Menu bar app Foreground process

Tip

Already using ARC on Kubernetes? Graftery uses the same protocol and the same runs-on: label convention. Your workflows don't need to change — just point a scale set name at your Mac and go.


Requirements

Requirement Details
macOS Sonoma or later
Tart brew install cirruslabs/cli/tart
Auth GitHub App credentials or a Personal Access Token
VM Tart image with the Actions runner binary & startup script (details)

macOS App

Installation

Download the latest DMG from the Releases page, open it, and drag Graftery into your Applications folder.

That's it — no dependencies beyond Tart.

Tip

Building from source? See Building from Source at the bottom of this page.

Quick Start

  1. Launch Graftery from Applications (or Spotlight).
  2. The setup wizard walks you through creating your first runner configuration — name it, enter your GitHub credentials, choose a base VM image, and set runner limits.
Setup wizard — name your configuration Setup wizard — authentication
Step 1 — Name your configuration Step 3 — Authentication
  1. The runner connects to GitHub and begins listening for jobs automatically.
  2. The menu bar icon shows live status. Click it to start/stop runners, add new configurations, or open the management window.

Menu bar dropdown

  1. Open Manage Configurations for the full editor — tabbed settings, live CPU & memory charts, and a built-in log viewer.

Configuration editor with metrics

Configuration

Each runner configuration is stored as a YAML file in ~/Library/Application Support/graftery/configs/. You can manage everything through the UI — the setup wizard for new configs, and the tabbed editor for changes (auto-saved on every edit).

You can also edit the YAML files directly with any text editor if you prefer.

Config file reference

# ── GitHub target ────────────────────────────────────────
url:  https://github.com/your-org        # org or repo URL
name: macos-runner                        # scale set name (= runs-on: label)

# ── Authentication (choose one) ─────────────────────────
# Option A: GitHub App
app_client_id:         "Iv1.abc123"
app_installation_id:   12345678
app_private_key_path:  /path/to/private-key.pem
# Or inline:
# app_private_key: |
#   -----BEGIN RSA PRIVATE KEY-----
#   ...

# Option B: Personal Access Token
# token: ghp_xxxxxxxxxxxx

# ── Runner settings ──────────────────────────────────────
base_image:    ghcr.io/cirruslabs/macos-runner:sonoma
max_runners:   2          # Apple allows max 2 macOS VMs per host
min_runners:   0          # warm-pool size
runner_group:  default
runner_prefix: runner     # used for orphan detection on startup
# labels:                 # defaults to scale set name
#   - macos
#   - sonoma

# ── Provisioning ─────────────────────────────────────────
# tart_path: /opt/homebrew/bin/tart
# provisioning:
#   scripts_dir: /path/to/custom/scripts
#   skip_builtin_scripts: false
#   prepared_image_name: ""

# ── Logging ──────────────────────────────────────────────
log_level:  info          # debug | info | warn | error
log_format: text          # text | json

CLI

Installation

Download the latest graftery binary from the Releases page and place it somewhere in your PATH.

# Example: install to /usr/local/bin
curl -fSL https://github.com/diranged/graftery/releases/latest/download/graftery-darwin-arm64 \
  -o /usr/local/bin/graftery
chmod +x /usr/local/bin/graftery

Tip

Building from source? See Building from Source at the bottom of this page.

Usage

# Using a config file
graftery --config /path/to/config.yaml

# Using individual flags
graftery \
  --url        https://github.com/your-org \
  --name       macos-runner \
  --app-client-id       Iv1.abc123 \
  --app-installation-id 12345678 \
  --app-private-key-path /path/to/private-key.pem \
  --base-image ghcr.io/cirruslabs/macos-runner:sonoma \
  --max-runners 2

# Using a PAT instead of a GitHub App
graftery \
  --url   https://github.com/your-org \
  --name  macos-runner \
  --token ghp_xxxxxxxxxxxx \
  --base-image ghcr.io/cirruslabs/macos-runner:sonoma

When --config is provided, the file is loaded first and any additional flags override its values.

CLI Flags

Flag Req Default Description
--config Path to YAML config file
--url yes GitHub org or repo URL
--name yes Scale set name (runs-on: label)
--app-client-id * GitHub App Client ID
--app-installation-id * GitHub App Installation ID
--app-private-key-path * Path to PEM file
--app-private-key * PEM contents inline
--token * Personal access token
--base-image ghcr.io/cirruslabs/macos-runner:sonoma Tart VM image
--max-runners 2 Max concurrent VMs
--min-runners 0 Warm pool size
--labels (same as --name) Additional labels
--runner-group default Runner group name
--runner-prefix runner VM name prefix
--log-level info debug / info / warn / error
--log-format text text / json

* Provide either GitHub App credentials or --token.

Logging

Logs go to stderr by default. Use --log-level debug for verbose output, or --log-format json for structured logs.


Image Provisioning

Applies to both the macOS app and CLI.

Graftery automatically bakes a prepared VM image from your base Tart image. The first run (or whenever scripts change) triggers provisioning:

 Base image  ──▶  Clone  ──▶  Boot  ──▶  Run bake.d/* scripts  ──▶  Save prepared image
                                              (lexicographic order)

A content hash of all scripts is cached — subsequent runs skip provisioning if nothing changed.

Built-in scripts

Script Purpose
01 Installs arc-runner-startup.sh — reads JIT config, starts runner, shuts down when done
02 Generates .setup_info — VM info shown in GitHub Actions "Set up job" step
03 Installs pre/post job hooks via ACTIONS_RUNNER_HOOK_JOB_STARTED / COMPLETED

Custom provisioning scripts

Drop your own scripts into the user scripts directory:

~/Library/Application Support/graftery/scripts/
  bake.d/
    50-install-tools.sh           # brew install jq terraform
    60-setup-xcode.sh             # sudo xcode-select -s ...
  hooks/
    pre.d/
      50-start-metrics.sh        # custom pre-job hook
    post.d/
      50-emit-metrics.sh         # custom post-job hook

Note

Merge behavior: User scripts merge with built-ins. Same-name files override. Execution is lexicographic (50-* runs after 01-* through 03-*).

Override the directory:

provisioning:
  scripts_dir: /path/to/custom/scripts

Forcing reprovisioning

graftery --reprovision --config config.yaml           # force a fresh bake
graftery --skip-builtin-scripts --config config.yaml  # only run user scripts

Pre/post job hooks

Hooks use GitHub Actions' native runner hook mechanism and appear in the job UI as collapsible sections:

Hook type Location Visible in
Pre-job hooks/pre.d/*.sh "Set up runner"
Post-job hooks/post.d/*.sh "Complete runner"

Hooks receive standard Actions environment variables (GITHUB_REPOSITORY, GITHUB_RUN_ID, etc.).

Base VM image requirements

The base Tart image must include:

Component Note
GitHub Actions runner At ~/actions-runner/ — all cirruslabs/macos-runner images include this
Tart guest agent All non-vanilla Cirrus Labs images include this
python3 Required by the setup-info script

The default ghcr.io/cirruslabs/macos-runner:sonoma satisfies all requirements.

Example: adding a tool to the baked image

Need CocoaPods for your builds? Create a bake script:

# ~/Library/Application Support/graftery/scripts/bake.d/50-install-cocoapods.sh
#!/bin/bash
set -euo pipefail
export PATH="/Users/admin/.rbenv/shims:/Users/admin/.rbenv/bin:$PATH"
eval "$(rbenv init - 2>/dev/null)" || true
gem install cocoapods
sudo ln -sf "$(rbenv which pod)" /usr/local/bin/pod

Restart the runner — it detects the new script, reprovisions, and every future VM ships with pod.

More examples

See the examples/ directory:

Example Description
iOS / React Native CocoaPods, ccache, Expo prebuild, workflow caching for Pods and DerivedData

Troubleshooting

tart not found

The tart binary must be in your PATH:

brew install cirruslabs/cli/tart

Or specify the path explicitly via CLI flag or config:

graftery --tart-path /opt/homebrew/bin/tart --config config.yaml
tart_path: /opt/homebrew/bin/tart
Authentication errors
Error Fix
"either GitHub App credentials or --token is required" Provide one auth method
"specify either GitHub App credentials or --token, not both" Use only one method
Private key errors Check PEM path is correct and readable. For inline YAML, use | block scalar
Orphaned VMs

On startup, Graftery auto-removes VMs matching the runner prefix. To clean up manually:

tart list                         # list all VMs
tart stop  runner-abc12345        # stop
tart delete runner-abc12345       # delete
Scale set registration fails
  • Verify --url points to a valid GitHub org or repo
  • Ensure your GitHub App has the required permissions, or your PAT has admin:org (org-level) / repo (repo-level) scope
Max runners limit

Apple's virtualization framework allows max 2 concurrent macOS VMs per host. The default max_runners: 2 reflects this. Setting it higher may cause VM creation failures.

Logs
Mode Location
macOS App ~/Library/Logs/graftery/graftery.log (menu bar -> Open Logs)
CLI stderr — use --log-level debug for verbose output

Building from Source

Requires Go 1.26+ and Xcode command-line tools (for Swift UI and code signing).

make build-cli    # CLI binary only (no CGO, no Swift)
make build-app    # full macOS .app bundle
make build-dmg    # drag-and-drop DMG installer
make install      # → /Applications/Graftery.app
make clean        # remove build artifacts

All artifacts are placed in the build/ directory.

License

Apache License 2.0


Built for Apple silicon  ·  Powered by Tart  ·  Speaks actions/scaleset

About

A native macOS menu bar app that orchestrates ephemeral GitHub Actions runners on Apple Silicon using Tart VMs

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors