{"id":49293487,"url":"https://github.com/diranged/graftery","last_synced_at":"2026-04-26T02:02:38.749Z","repository":{"id":349660170,"uuid":"1203142051","full_name":"diranged/graftery","owner":"diranged","description":"A native macOS menu bar app that orchestrates ephemeral GitHub Actions runners on Apple Silicon using Tart VMs","archived":false,"fork":false,"pushed_at":"2026-04-07T02:17:41.000Z","size":520,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T02:22:11.190Z","etag":null,"topics":["apple-silicon","ephemeral-runners","github-actions","github-actions-runner","macos","menu-bar-app","self-hosted-runners","swiftui","tart","virtualization"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diranged.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-06T19:03:26.000Z","updated_at":"2026-04-07T02:13:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/diranged/graftery","commit_stats":null,"previous_names":["diranged/graftery"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/diranged/graftery","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diranged%2Fgraftery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diranged%2Fgraftery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diranged%2Fgraftery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diranged%2Fgraftery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diranged","download_url":"https://codeload.github.com/diranged/graftery/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diranged%2Fgraftery/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32283294,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"online","status_checked_at":"2026-04-26T02:00:05.962Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apple-silicon","ephemeral-runners","github-actions","github-actions-runner","macos","menu-bar-app","self-hosted-runners","swiftui","tart","virtualization"],"created_at":"2026-04-26T02:02:37.823Z","updated_at":"2026-04-26T02:02:38.740Z","avatar_url":"https://github.com/diranged.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"icons/icon.png\" alt=\"Graftery\" width=\"160\" height=\"160\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eGraftery\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eEphemeral macOS VMs for GitHub Actions — powered by \u003ca href=\"https://tart.run\"\u003eTart\u003c/a\u003e\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg alt=\"Platform\" src=\"https://img.shields.io/badge/platform-macOS_14+-0e6878?style=flat-square\u0026logo=apple\u0026logoColor=white\" /\u003e\n  \u003cimg alt=\"Protocol\" src=\"https://img.shields.io/badge/protocol-actions%2Fscaleset-094858?style=flat-square\u0026logo=github\u0026logoColor=white\" /\u003e\n  \u003cimg alt=\"Virtualization\" src=\"https://img.shields.io/badge/virt-Tart-c94a30?style=flat-square\u0026logo=apple\u0026logoColor=white\" /\u003e\n  \u003cimg alt=\"License\" src=\"https://img.shields.io/badge/license-Apache_2.0-1a8090?style=flat-square\" /\u003e\n\u003c/p\u003e\n\n---\n\n## Why Graftery?\n\nIf 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.\n\n**Graftery fixes this.** It brings the same ephemeral, scale-to-zero model that [Actions Runner Controller (ARC)](https://github.com/actions/actions-runner-controller) 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.\n\n### How it works\n\n```\nGitHub Actions                        Your Mac\n─────────────                         ────────\n  Job queued  ──── scaleset poll ────▶  Graftery sees demand\n                                        │\n                                        ├─ Clones base VM image\n                                        ├─ Injects JIT runner config\n                                        ├─ Boots ephemeral Tart VM\n                                        ├─ Runner picks up the job\n                                        └─ VM destroyed on completion\n```\n\nGraftery speaks the [actions/scaleset](https://github.com/actions/scaleset) protocol natively — the same wire protocol ARC uses. No custom API, no webhook glue.\n\n### Core capabilities\n\n**Clean room every job** — Each job runs in a fresh VM clone. No state leaks between jobs, ever.\n\n**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.\n\n**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.\n\n**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.\n\n**Orphan cleanup** — On startup, Graftery finds and removes VMs left behind by crashes. Session conflicts with GitHub are retried automatically with exponential backoff.\n\n**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.\n\n**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.\n\n### Two ways to run it\n\nGraftery 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.\n\n| | \u003cimg src=\"https://img.shields.io/badge/-macOS_App-0e6878?style=flat-square\u0026logo=apple\u0026logoColor=white\" /\u003e | \u003cimg src=\"https://img.shields.io/badge/-CLI-094858?style=flat-square\u0026logo=gnubash\u0026logoColor=white\" /\u003e |\n|:---|:---|:---|\n| **Best for** | Interactive use on a Mac with a display | Headless servers, automation, launchd/systemd |\n| **Install** | [Download DMG](#-macos-app) | [Download binary](#-cli) |\n| **Runner sets** | **Multiple** — manage unlimited independent configs | **One** per process |\n| **Config** | 6-step setup wizard + tabbed editor with auto-save | YAML file + CLI flags |\n| **Metrics** | Live time-series charts (CPU \u0026 memory), menu bar gauges | Prometheus `/metrics` endpoint |\n| **Logs** | Built-in log viewer with search, level filtering, color | Structured logs to stderr |\n| **Controls** | Menu bar start/stop per runner, enable/disable auto-start | SIGINT/SIGTERM |\n| **Runs as** | Menu bar app | Foreground process |\n\n\u003e [!TIP]\n\u003e **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.\n\n---\n\n## Requirements\n\n| Requirement | Details |\n|:---|:---|\n| ![macOS](https://img.shields.io/badge/-macOS_14+-0e6878?style=flat-square\u0026logo=apple\u0026logoColor=white) | Sonoma or later |\n| ![Tart](https://img.shields.io/badge/-Tart-094858?style=flat-square\u0026logoColor=white) | `brew install cirruslabs/cli/tart` |\n| ![Auth](https://img.shields.io/badge/-GitHub_Auth-c94a30?style=flat-square\u0026logo=github\u0026logoColor=white) | GitHub App credentials **or** a Personal Access Token |\n| ![VM](https://img.shields.io/badge/-Base_VM-1a8090?style=flat-square\u0026logoColor=white) | Tart image with the Actions runner binary \u0026 startup script ([details](#base-vm-image-requirements)) |\n\n---\n\n# \u003cimg src=\"https://img.shields.io/badge/-macOS_App-0e6878?style=for-the-badge\u0026logo=apple\u0026logoColor=white\" alt=\"macOS App\" /\u003e\n\n## Installation\n\nDownload the latest **DMG** from the [Releases](https://github.com/diranged/graftery/releases) page, open it, and drag **Graftery** into your Applications folder.\n\nThat's it — no dependencies beyond [Tart](#requirements).\n\n\u003e [!TIP]\n\u003e Building from source? See [Building from Source](#building-from-source) at the bottom of this page.\n\n## Quick Start\n\n1. **Launch Graftery** from Applications (or Spotlight).\n2. 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.\n\n\u003ctable align=\"center\"\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\" valign=\"top\"\u003e\u003cimg src=\"docs/screenshots/wizard-name.png\" alt=\"Setup wizard — name your configuration\" width=\"400\" /\u003e\u003c/td\u003e\n\u003ctd align=\"center\" valign=\"top\"\u003e\u003cimg src=\"docs/screenshots/wizard-auth.png\" alt=\"Setup wizard — authentication\" width=\"400\" /\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003csub\u003eStep 1 — Name your configuration\u003c/sub\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003csub\u003eStep 3 — Authentication\u003c/sub\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n3. The runner connects to GitHub and begins listening for jobs automatically.\n4. The **menu bar icon** shows live status. Click it to start/stop runners, add new configurations, or open the management window.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/menu-bar.png\" alt=\"Menu bar dropdown\" width=\"220\" /\u003e\n\u003c/p\u003e\n\n5. Open **Manage Configurations** for the full editor — tabbed settings, live CPU \u0026 memory charts, and a built-in log viewer.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/config-editor.png\" alt=\"Configuration editor with metrics\" width=\"700\" /\u003e\n\u003c/p\u003e\n\n## Configuration\n\nEach 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).\n\nYou can also edit the YAML files directly with any text editor if you prefer.\n\n### Config file reference\n\n```yaml\n# ── GitHub target ────────────────────────────────────────\nurl:  https://github.com/your-org        # org or repo URL\nname: macos-runner                        # scale set name (= runs-on: label)\n\n# ── Authentication (choose one) ─────────────────────────\n# Option A: GitHub App\napp_client_id:         \"Iv1.abc123\"\napp_installation_id:   12345678\napp_private_key_path:  /path/to/private-key.pem\n# Or inline:\n# app_private_key: |\n#   -----BEGIN RSA PRIVATE KEY-----\n#   ...\n\n# Option B: Personal Access Token\n# token: ghp_xxxxxxxxxxxx\n\n# ── Runner settings ──────────────────────────────────────\nbase_image:    ghcr.io/cirruslabs/macos-runner:sonoma\nmax_runners:   2          # Apple allows max 2 macOS VMs per host\nmin_runners:   0          # warm-pool size\nrunner_group:  default\nrunner_prefix: runner     # used for orphan detection on startup\n# labels:                 # defaults to scale set name\n#   - macos\n#   - sonoma\n\n# ── Provisioning ─────────────────────────────────────────\n# tart_path: /opt/homebrew/bin/tart\n# provisioning:\n#   scripts_dir: /path/to/custom/scripts\n#   skip_builtin_scripts: false\n#   prepared_image_name: \"\"\n\n# ── Logging ──────────────────────────────────────────────\nlog_level:  info          # debug | info | warn | error\nlog_format: text          # text | json\n```\n\n---\n\n# \u003cimg src=\"https://img.shields.io/badge/-CLI-094858?style=for-the-badge\u0026logo=gnubash\u0026logoColor=white\" alt=\"CLI\" /\u003e\n\n## Installation\n\nDownload the latest **`graftery` binary** from the [Releases](https://github.com/diranged/graftery/releases) page and place it somewhere in your `PATH`.\n\n```bash\n# Example: install to /usr/local/bin\ncurl -fSL https://github.com/diranged/graftery/releases/latest/download/graftery-darwin-arm64 \\\n  -o /usr/local/bin/graftery\nchmod +x /usr/local/bin/graftery\n```\n\n\u003e [!TIP]\n\u003e Building from source? See [Building from Source](#building-from-source) at the bottom of this page.\n\n## Usage\n\n```bash\n# Using a config file\ngraftery --config /path/to/config.yaml\n\n# Using individual flags\ngraftery \\\n  --url        https://github.com/your-org \\\n  --name       macos-runner \\\n  --app-client-id       Iv1.abc123 \\\n  --app-installation-id 12345678 \\\n  --app-private-key-path /path/to/private-key.pem \\\n  --base-image ghcr.io/cirruslabs/macos-runner:sonoma \\\n  --max-runners 2\n\n# Using a PAT instead of a GitHub App\ngraftery \\\n  --url   https://github.com/your-org \\\n  --name  macos-runner \\\n  --token ghp_xxxxxxxxxxxx \\\n  --base-image ghcr.io/cirruslabs/macos-runner:sonoma\n```\n\nWhen `--config` is provided, the file is loaded first and any additional flags override its values.\n\n## CLI Flags\n\n| Flag | Req | Default | Description |\n|:---|:---:|:---|:---|\n| `--config` | | | Path to YAML config file |\n| `--url` | **yes** | | GitHub org or repo URL |\n| `--name` | **yes** | | Scale set name (`runs-on:` label) |\n| `--app-client-id` | \\* | | GitHub App Client ID |\n| `--app-installation-id` | \\* | | GitHub App Installation ID |\n| `--app-private-key-path` | \\* | | Path to PEM file |\n| `--app-private-key` | \\* | | PEM contents inline |\n| `--token` | \\* | | Personal access token |\n| `--base-image` | | `ghcr.io/cirruslabs/macos-runner:sonoma` | Tart VM image |\n| `--max-runners` | | `2` | Max concurrent VMs |\n| `--min-runners` | | `0` | Warm pool size |\n| `--labels` | | _(same as `--name`)_ | Additional labels |\n| `--runner-group` | | `default` | Runner group name |\n| `--runner-prefix` | | `runner` | VM name prefix |\n| `--log-level` | | `info` | `debug` / `info` / `warn` / `error` |\n| `--log-format` | | `text` | `text` / `json` |\n\n\\* Provide **either** GitHub App credentials **or** `--token`.\n\n## Logging\n\nLogs go to stderr by default. Use `--log-level debug` for verbose output, or `--log-format json` for structured logs.\n\n---\n\n# \u003cimg src=\"https://img.shields.io/badge/-Image_Provisioning-1a8090?style=for-the-badge\u0026logoColor=white\" alt=\"Image Provisioning\" /\u003e\n\n_Applies to both the macOS app and CLI._\n\nGraftery automatically **bakes** a prepared VM image from your base Tart image. The first run (or whenever scripts change) triggers provisioning:\n\n```\n Base image  ──▶  Clone  ──▶  Boot  ──▶  Run bake.d/* scripts  ──▶  Save prepared image\n                                              (lexicographic order)\n```\n\nA content hash of all scripts is cached — subsequent runs skip provisioning if nothing changed.\n\n## Built-in scripts\n\n| Script | Purpose |\n|:---|:---|\n| ![01](https://img.shields.io/badge/01-startup--script.sh-094858?style=flat-square) | Installs `arc-runner-startup.sh` — reads JIT config, starts runner, shuts down when done |\n| ![02](https://img.shields.io/badge/02-setup--info.py-094858?style=flat-square) | Generates `.setup_info` — VM info shown in GitHub Actions \"Set up job\" step |\n| ![03](https://img.shields.io/badge/03-runner--hooks.sh-094858?style=flat-square) | Installs pre/post job hooks via `ACTIONS_RUNNER_HOOK_JOB_STARTED` / `COMPLETED` |\n\n## Custom provisioning scripts\n\nDrop your own scripts into the user scripts directory:\n\n```\n~/Library/Application Support/graftery/scripts/\n  bake.d/\n    50-install-tools.sh           # brew install jq terraform\n    60-setup-xcode.sh             # sudo xcode-select -s ...\n  hooks/\n    pre.d/\n      50-start-metrics.sh        # custom pre-job hook\n    post.d/\n      50-emit-metrics.sh         # custom post-job hook\n```\n\n\u003e [!NOTE]\n\u003e **Merge behavior:** User scripts merge with built-ins. Same-name files override. Execution is lexicographic (`50-*` runs after `01-*` through `03-*`).\n\nOverride the directory:\n\n```yaml\nprovisioning:\n  scripts_dir: /path/to/custom/scripts\n```\n\n## Forcing reprovisioning\n\n```bash\ngraftery --reprovision --config config.yaml           # force a fresh bake\ngraftery --skip-builtin-scripts --config config.yaml  # only run user scripts\n```\n\n## Pre/post job hooks\n\nHooks use GitHub Actions' native runner hook mechanism and appear in the job UI as collapsible sections:\n\n| Hook type | Location | Visible in |\n|:---|:---|:---|\n| **Pre-job** | `hooks/pre.d/*.sh` | \"Set up runner\" |\n| **Post-job** | `hooks/post.d/*.sh` | \"Complete runner\" |\n\nHooks receive standard Actions environment variables (`GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, etc.).\n\n## Base VM image requirements\n\nThe base Tart image must include:\n\n| Component | Note |\n|:---|:---|\n| **GitHub Actions runner** | At `~/actions-runner/` — all `cirruslabs/macos-runner` images include this |\n| **Tart guest agent** | All non-vanilla Cirrus Labs images include this |\n| **python3** | Required by the setup-info script |\n\n\u003e The default `ghcr.io/cirruslabs/macos-runner:sonoma` satisfies all requirements.\n\n## Example: adding a tool to the baked image\n\nNeed CocoaPods for your builds? Create a bake script:\n\n```bash\n# ~/Library/Application Support/graftery/scripts/bake.d/50-install-cocoapods.sh\n#!/bin/bash\nset -euo pipefail\nexport PATH=\"/Users/admin/.rbenv/shims:/Users/admin/.rbenv/bin:$PATH\"\neval \"$(rbenv init - 2\u003e/dev/null)\" || true\ngem install cocoapods\nsudo ln -sf \"$(rbenv which pod)\" /usr/local/bin/pod\n```\n\nRestart the runner — it detects the new script, reprovisions, and every future VM ships with `pod`.\n\n## More examples\n\nSee the [`examples/`](examples/) directory:\n\n| Example | Description |\n|:---|:---|\n| [iOS / React Native](examples/ios-react-native/) | CocoaPods, ccache, Expo prebuild, workflow caching for Pods and DerivedData |\n\n---\n\n# \u003cimg src=\"https://img.shields.io/badge/-Troubleshooting-c94a30?style=for-the-badge\u0026logoColor=white\" alt=\"Troubleshooting\" /\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e\u003ccode\u003etart\u003c/code\u003e not found\u003c/strong\u003e\u003c/summary\u003e\n\nThe `tart` binary must be in your PATH:\n\n```bash\nbrew install cirruslabs/cli/tart\n```\n\nOr specify the path explicitly via CLI flag or config:\n\n```bash\ngraftery --tart-path /opt/homebrew/bin/tart --config config.yaml\n```\n\n```yaml\ntart_path: /opt/homebrew/bin/tart\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eAuthentication errors\u003c/strong\u003e\u003c/summary\u003e\n\n| Error | Fix |\n|:---|:---|\n| _\"either GitHub App credentials or --token is required\"_ | Provide one auth method |\n| _\"specify either GitHub App credentials or --token, not both\"_ | Use only one method |\n| Private key errors | Check PEM path is correct and readable. For inline YAML, use `\\|` block scalar |\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eOrphaned VMs\u003c/strong\u003e\u003c/summary\u003e\n\nOn startup, Graftery auto-removes VMs matching the runner prefix. To clean up manually:\n\n```bash\ntart list                         # list all VMs\ntart stop  runner-abc12345        # stop\ntart delete runner-abc12345       # delete\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eScale set registration fails\u003c/strong\u003e\u003c/summary\u003e\n\n- Verify `--url` points to a valid GitHub org or repo\n- Ensure your GitHub App has the required permissions, or your PAT has `admin:org` (org-level) / `repo` (repo-level) scope\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eMax runners limit\u003c/strong\u003e\u003c/summary\u003e\n\nApple'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.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eLogs\u003c/strong\u003e\u003c/summary\u003e\n\n| Mode | Location |\n|:---|:---|\n| **macOS App** | `~/Library/Logs/graftery/graftery.log` (menu bar -\u003e Open Logs) |\n| **CLI** | stderr — use `--log-level debug` for verbose output |\n\n\u003c/details\u003e\n\n---\n\n## Building from Source\n\nRequires **Go 1.26+** and **Xcode command-line tools** (for Swift UI and code signing).\n\n```bash\nmake build-cli    # CLI binary only (no CGO, no Swift)\nmake build-app    # full macOS .app bundle\nmake build-dmg    # drag-and-drop DMG installer\nmake install      # → /Applications/Graftery.app\nmake clean        # remove build artifacts\n```\n\nAll artifacts are placed in the `build/` directory.\n\n## License\n\n[Apache License 2.0](LICENSE)\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003eBuilt for Apple silicon \u0026nbsp;·\u0026nbsp; Powered by \u003ca href=\"https://tart.run\"\u003eTart\u003c/a\u003e \u0026nbsp;·\u0026nbsp; Speaks \u003ca href=\"https://github.com/actions/scaleset\"\u003eactions/scaleset\u003c/a\u003e\u003c/sub\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiranged%2Fgraftery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiranged%2Fgraftery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiranged%2Fgraftery/lists"}