Skip to content
el-jPublic

About

RunZero is the answer if action minutes run low!

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

231 Commits

Folders and files

Repository files navigation

⚡ RunZero

Zero Cloud Minutes. Zero K8s Bloat. Zero Idle Waste.

An intelligent, autoscaling local GitHub Actions runner fleet with Dual-Engine execution (Docker Containers & Dedicated Linux VMs via OrbStack, Windows WSL2, and Multipass), with native Apple Silicon (arm64) & Intel/AMD (amd64), Verdaccio, Athens, apt-cacher-ng, and persistent multi-language caching.

License: MIT Docker OrbStack Apple Silicon GitHub Actions PRs Welcome


📖 Table of Contents


💡 Why RunZero?

Feature / Metric ⚡ RunZero 🏢 Actions Runner Controller (ARC) 🎭 nektos / act 📦 Static Docker Runner
Target Environment Local Mac / Windows / Linux Enterprise Cloud K8s Fleet Local CLI Scratchpad Single Homelab Container
Idle RAM Overhead ~20 MB (0 MB at idle) 1.5 GB – 3.5 GB (Heavy K8s) 0 MB (Manual CLI only) ~500 MB+ (Always running)
Execution Engines Dual-Engine (Docker + VM) Containers Only (K8s Pods) Containers Only Container Only
Runs Real GitHub Queue Yes (Pulls live jobs) Yes ❌ No (Local emulation only) Yes
Personal Account Multi-Repo Automatic Discovery ❌ No (Org / Enterprise only) ❌ N/A ❌ No (1 repo per container)
Multi-Arch (Apple Silicon + x86_64) Native ARM64 + Rosetta AMD64 ⚠️ Complex (K8s node taints) ⚠️ Partial ❌ Fixed single arch
Built-in Package Proxies APT + Verdaccio + Athens + Mirrors ❌ None (Requires K8s PVC/NFS) ❌ None ❌ None
Setup Complexity 1 Command (make start) ❌ Complex Helm / CRDs Simple CLI ⚠️ Moderate

🚀 Dual-Engine Execution (Containers vs. Full VMs)

RunZero is the first local runner fleet that gives you the choice between ultra-lightweight Docker containers and dedicated Linux Virtual Machines:

Engine Backend Upstream Runtime Best Used For Verification Status
🐳 Docker Containers (RUNNER_BACKEND=docker) Docker Engine / OrbStack Fast unit tests, linting, build pipelines, JS/Node/Go steps (instant ~0.3s boot, ~20MB RAM). ✅ Unit-tested + automated e2e in CI
💻 OrbStack Linux Machines (RUNNER_BACKEND=orbstack-vm) OrbStack Virtualization Full systemd support, background daemons, headless Chrome/Lighthouse, unconfined Docker daemon. ✅ Unit-tested + manually verified against real OrbStack VMs
🪟 Windows WSL2 (RUNNER_BACKEND=wsl2) Windows Subsystem for Linux 2 Native Linux VM execution on Windows 10/11 & Windows Server. ⚠️ Unit-tested only — not yet verified against a real Windows/WSL2 host
🐧 Canonical Multipass (RUNNER_BACKEND=multipass) Canonical Multipass Universal cross-platform VM backend for macOS, Linux, and Windows. ⚠️ Unit-tested only — not yet verified against a real Multipass install

The WSL2 and Multipass drivers register and run ephemeral runners and are covered by unit tests that verify command construction (every subprocess call to wsl.exe / multipass is mocked at the call site), but neither has been exercised against real hardware — this project has been developed and driven entirely from a macOS host with OrbStack. See E2E_TESTING.md and issue #12 for the full honest breakdown and how to help verify one of these backends for real.


🧠 Hybrid Auto-Routing

You don't have to choose just one! With RUNNER_BACKEND=auto and AUTO_ROUTE_VM=true, RunZero automatically inspects queued jobs and routes them intelligently:

# 1. Standard build job -> Automatically runs in ultra-fast Docker container (~0.3s)
jobs:
  unit-tests:
    runs-on: [ self-hosted, local ]
    steps:
      - run: npm test

# 2. Browser / Lighthouse / Systemd job -> Automatically routed to dedicated Linux VM!
jobs:
  e2e-lighthouse:
    runs-on: [ self-hosted, local, browser ]  # or 'vm', 'e2e', 'lighthouse', 'systemd'
    steps:
      - run: npx lhci autorun

🖥️ How VM Runners Are Built & Registered

VM-routed jobs (E2E, Lighthouse, anything needing a real Docker daemon or systemd) run in an ephemeral OrbStack VM cloned (orbctl clone) from a golden base image runzero-vm-base-<arch>:

  • Golden image (make build-vm-base, also run by make start): Docker engine, the shared toolchain from docker/provision-toolchain.sh (Node via nvm, pnpm, .NET, Chrome, Playwright deps, libatomic1, ...) and the Actions runner in /home/runner/actions-runner. It is stamped with the runner version and a hash of the provisioning script, so editing the script marks it stale and the next build refreshes it. Golden images are templates: they are never listed as runners and never pruned.
  • Per job: the bridge clones the image, starts it, then registers an --ephemeral runner with the job's labels (local, arch). If registration fails the VM is deleted rather than left idle. localhost is made IPv4-only in the VM (GitHub-hosted behaviour) so services bound to 127.0.0.1 work. VM names are sanitised to OrbStack's hostname rules; the repo mapping is kept in the bridge's persisted instance store (~/.local-github-runner/state/bridge-instances.json).
  • Rate limits: GitHub list calls use ETag revalidation (304 is free), calls stop while the quota is exhausted, and the completed-jobs history sweep runs in the background every 5 minutes so it can never delay job detection.

📦 Proxy Registries & Upstream Ecosystem

RunZero runs local caching proxy registries alongside the autoscaler so your runners and image builds never re-download packages over the public internet:

Service / Dependency Upstream Project Local Port Dashboard / Report Description
APT Cacher NG apt-cacher-ng :49503 http://localhost:49503/acng-report.html Caches Ubuntu/Debian .deb packages across Docker builds & VM golden image clones.
Verdaccio Verdaccio :49501 http://localhost:49501 Private npm/yarn/pnpm caching proxy registry with web UI.
Athens Athens Go Proxy :49500 http://localhost:49500 Immutable Go module proxy and download cache.
Docker Registry Mirror Docker Registry :49502 http://localhost:49502 Pull-through mirror for Docker Hub images.
devpi devpi-server :49507 http://localhost:49507/root/pypi/+simple/ Real pull-through caching proxy for pip/uv (PyPI) via its built-in root/pypi mirror index.
kellnr kellnr :49506 http://localhost:49506 Real pull-through caching proxy for Cargo/crates.io via kellnr's built-in crates.io proxy.
Node.js Toolchain NVM & Node.js Local Pre-baked Pre-installed Node.js 20 LTS, 22 LTS, and 24 Current with yarn & pnpm.
.NET SDK Microsoft .NET 8 Local Pre-baked Pre-installed .NET 8.0 SDK for C#/F# workflow pipelines.
Browser Testing Playwright & Google Chrome Local Pre-baked Pre-installed system dependencies and browser runtimes for E2E testing.

Cargo has no single "index URL" env var the way pip/uv (PIP_INDEX_URL/UV_INDEX_URL) and Go (GOPROXY) do -- pointing it at kellnr requires a real ~/.cargo/config.toml source-replacement block (written automatically by docker/start.sh for Docker-engine runners, and by OrbStackVMDriver for VM-engine runners); CARGO_SOURCE_* env vars for a custom [source.*] table are silently ignored by Cargo. Likewise, pip refuses a plain-HTTP index on any host other than localhost/127.0.0.1 unless that host is explicitly trusted via PIP_TRUSTED_HOST -- both runner engines set this automatically alongside PIP_INDEX_URL wherever the index isn't reached via localhost; uv has no equivalent restriction.

🪐 OrbStack VM Local Disk Caching

The proxy registries above cache package downloads over the network. Package managers also keep a local, already-extracted disk cache (~/.npm, ~/.cache/pip, ~/go/pkg, ~/.cargo/registry, etc.) so a second job using the same package doesn't even need the network proxy -- this is what HOST_CACHE_DIR and the Cache Analytics dashboard panel track. For the Docker engine this is a plain -v host:container bind mount. An OrbStack VM is a real, separate guest filesystem with no such flag, but every non-isolated OrbStack VM (the kind this project creates) automatically virtiofs-shares the entire host macOS filesystem into the guest at a fixed path, /mnt/mac<absolute-macOS-path> -- confirmed live: a file written from inside a VM under /mnt/mac/Users/... appears immediately, with matching ownership, at the real /Users/... path on the host, and vice versa. OrbStackVMDriver uses this to mount --bind each host-side cache directory onto its container-style destination path inside the VM before the job runs, so cache data written by one ephemeral VM is really on host disk and visible to the next VM cloned for the same architecture. If a host cache directory isn't visible via the mac share for some reason, the mount is skipped with a warning rather than failing the job outright.


🐳 Docker & VM Engine Compatibility

Engine macOS (Apple Silicon) macOS (Intel) Windows Linux / Homelab
🪐 OrbStack (Recommended for Mac) ⭐ Containers + Linux VMs ⭐ Fast — —
🐳 Docker Desktop Native Containers Native Containers Native Containers Native Containers
🪟 Windows WSL2 — — ⭐ Native Linux VMs —
🐧 Native Linux Docker Native (ARM64) Native (AMD64) — ⭐ Native (Best for Servers)
🚀 Canonical Multipass Native VMs Native VMs Native VMs Native VMs

🌟 Key Features

  1. Intelligent Dynamic Autoscaling:
    • Watches your GitHub queue for unclaimed jobs.
    • Spins up ephemeral instances (--ephemeral) on-demand and tears them down immediately upon job completion (0 MB RAM consumed when idle).
  2. Owner-Wide Multi-Repo Auto-Discovery:
    • Automatically monitors all active repositories under your personal GitHub account (OWNER=your-username), explicit repo lists, or GitHub Organizations.
  3. Pluggable Multi-Backend Drivers:
  4. Golden VM Base Cloning (make build-vm-base):
    • Pre-bakes the unified toolchain into a golden base image (runzero-vm-base-<arch>). VM jobs launch in seconds by cloning the base image.
  5. Self-Healing Zombie Runner Reconciliation:
    • Automatically detects offline runners that GitHub still marks as busy: true (e.g. after abrupt host restarts), cancels orphaned jobs, and unblocks your queue.
  6. Adaptive GitHub API Rate-Limiting:
    • Intelligently filters to active repositories and dynamically paces API calls to keep you safely within GitHub's 5,000 req/hr ceiling.
  7. Built-in Local Proxy Registries & Caching:
    • apt-cacher-ng (:49503) for Debian/Ubuntu .deb packages.
    • Verdaccio (:49501) for instant npm/yarn/pnpm caching + web dashboard.
    • Athens (:49500) for immutable Go module caching.
    • Docker Registry Mirror (:49502) for Docker Hub pull-through caching.
    • devpi (:49507) for pip/uv PyPI pull-through caching.
    • kellnr (:49506) for Cargo/crates.io pull-through caching.
    • Per-VM local disk caches (~/.npm, ~/.cache/pip, ~/go/pkg, ~/.cargo/registry, etc.) are real even for the OrbStack VM engine, bind-mounted from host storage via OrbStack's automatic /mnt/mac filesystem share -- see "OrbStack VM Local Disk Caching" below.
  8. Zero Cloud Bill:
    • Self-hosted runners never consume GitHub Actions minutes (100% free and unlimited).

🏗️ Architecture Overview

                          ┌──────────────────────────────────────────────┐
                          │         RunZero Autoscaler Daemon            │
                          │   (Queue Monitor, Rate Limiting, Discovery)  │
                          └──────────────────────┬───────────────────────┘
                                                 │
                                     Intelligent Auto-Router
                                 (Inspects Job Labels & Platform)
                                                 │
                       ┌─────────────────────────┴─────────────────────────┐
                       ▼                                                   ▼
         ┌───────────────────────────┐                       ┌───────────────────────────┐
         │     Container Driver      │                       │     Pluggable VM Driver   │
         │ (runs-on: [self-hosted])  │                       │ (runs-on: [..., vm, e2e]) │
         └─────────────┬─────────────┘                       └─────────────┬─────────────┘
                       │                                                   │
         ┌─────────────┴─────────────┐                       ┌─────────────┴─────────────┐
         ▼                           ▼                       ▼             ▼             ▼
   ┌───────────┐               ┌───────────┐           ┌───────────┐ ┌───────────┐ ┌───────────┐
   │ ARM64 Run │               │ AMD64 Run │           │ OrbStack  │ │ Windows   │ │Multipass  │
   │ (Docker)  │               │ (Rosetta) │           │ Machine   │ │ WSL2/VM   │ │(Cross-OS) │
   └───────────┘               └───────────┘           └───────────┘ └───────────┘ └───────────┘

📡 Dashboard Live Contract (State, Retention, Retry)

The dashboard is driven directly from the /api/fleet snapshot plus /api/cache and /api/logs support endpoints.

  • Fleet snapshot (/api/fleet) includes:
    • Active runners, queue, and completed jobs
    • Capacity (busy_runners, free_slots, max_runners)
    • Repo priority / paused repos
    • Rate-limit and Actions billing telemetry
  • Completed job retention:
    • Up to 100 completed jobs are retained in memory (pkg/state/history.go)
    • Jobs are sorted newest-first by completed_at
  • Failure diagnosis parity:
    • Failed/timed-out/cancelled jobs include a human-readable failure_reason
    • failed_step and top annotation messages are included where available
  • Runner cleanup evidence:
    • Before finished runners are pruned, the daemon retains the last runner log tail
    • Up to 100 runner log tails are retained, each capped at 16 KiB
  • Retry semantics:
    • The UI calls /api/actions/workflow with rerun or rerun-failed
    • Retry actions mark history as dirty so the next autoscaler cycle bypasses the normal refresh throttle and updates completed-job history immediately
  • Placeholder/TODO audit (dashboard + backend shipped paths):
    • Audit command: rg "TODO|FIXME|stub|placeholder" web/src pkg --glob "**/*.{ts,tsx,go}"
    • Result: no unresolved TODO/FIXME/stub markers in shipped TypeScript/Go sources (remaining placeholder=... matches are HTML input placeholder attributes)

📁 Repository Structure

.
├── cmd/runzero/                           # ⚙️ CLI entrypoints (daemon, bridge, doctor, VM base build)
├── pkg/                                   # 🧠 Go engine modules (api, daemon, driver, github, state, reaper, ...)
├── web/                                   # 🖥️ TypeScript/Vite dashboard (live fleet UI + API client)
│   ├── src/                               #    Dashboard TS, styles, generated OpenAPI types
│   ├── dist/                              #    Production bundle served by the Go daemon
│   └── package.json                       #    Dashboard dependencies and scripts
├── docker/                                # 🐳 Container Build Manifests & Entrypoints
│   ├── Dockerfile                         #    Multi-arch runner image (ARM64 + AMD64)
│   ├── Dockerfile.autoscaler              #    Autoscaler daemon container
│   ├── Dockerfile.devpi                   #    devpi pip/uv PyPI proxy image (no maintained multi-arch upstream)
│   ├── provision-toolchain.sh             #    Unified toolchain script (shared by Docker & VM base images)
│   └── start.sh                           #    Runner entrypoint with proxy auto-detect
├── spec/                                  # 📐 TypeSpec API contract + generated OpenAPI artifacts
├── website/                               # 🚀 Astro Static Website & Documentation
│   ├── src/                               #    Astro components, pages (Hero + Docs + Versions) & styles
│   ├── e2e/                               #    Playwright E2E tests (website + dashboard lifecycle flows)
│   ├── public/                            #    Self-hosted fonts, versions.json, and SVG assets
│   ├── astro.config.mjs                   #    Astro static SSG configuration
│   └── package.json                       #    Website dependencies
├── docs/                                  # 🌐 Compiled GitHub Pages Website
│   ├── index.html                         #    Compiled Hero Landing Page
│   ├── docs/index.html                    #    Compiled Dedicated Documentation Page
│   └── versions/index.html                #    Compiled Release Version Archive Page
├── docker-compose.yml                     # 🚀 Orchestration (apt-cacher + Verdaccio + Athens + Docker Mirror + devpi + kellnr)
├── Makefile                               # 🛠️ Unified management commands
├── .env.example                           # ⚙️ Configuration template
├── CONTRIBUTING.md                        # 🤝 Contributor guidelines & Git-Flow guide
├── LICENSE                                # 📄 MIT License
└── README.md                              # 📖 Main repository documentation

⚡ Quick Start

Warning

Self-hosted runners execute workflow code with near-root access to this machine. Only use RunZero for repositories whose contributors you trust, and never for public repositories that run fork pull requests. Read the threat model first.

1. Initialize environment:

make env

Edit .env and configure:

ACCESS_TOKEN=ghp_yourPersonalAccessTokenHere
OWNER=your-username
AUTO_DISCOVER_REPOS=true
RUNNER_BACKEND=auto  # 'auto', 'docker', 'orbstack-vm', 'wsl2', or 'multipass'
RUNNER_ARCH=both     # 'both', 'amd64', or 'arm64'
PROXIES_ENABLED=true # Starts apt-cacher, Verdaccio, Athens & Docker Mirror

2. Build images & VM templates:

make build          # Builds ARM64 + AMD64 + Autoscaler container images
make build-vm-base  # Builds golden OrbStack VM base image for instant cloning

3. Start the Autoscaler & Proxies:

make start          # Starts apt-cacher, Verdaccio, Athens, Docker mirror, and Autoscaler

4. Check status & live logs:

make status         # View active autoscaler, proxies, and dynamic runner instances
make logs           # Stream live autoscaler logs

5. Stop the Autoscaler & Proxies:

make stop           # (or make down)

6. Enable GitHub Actions Minutes Panel (Optional but Recommended)

The dashboard has a dedicated GitHub Actions Minutes panel. It requires billing API access for the selected owner or org.

  1. Configure your scope in .env:
OWNER=your-username
# ORG=your-org-name
  1. Use ACCESS_TOKEN with repository access and billing-read access for that target scope.
  2. Rebuild/restart:
docker compose build autoscaler
make restart
  1. Open dashboard: http://localhost:49505 and verify panel status says Billing synced from GitHub.

Full setup guide: ACTIONS_BILLING_HOWTO.md


🧪 Testing & Quality Suite

make check is the single quality gate, run identically by CI and the pre-push hook. It runs go vet + go test -race -cover, the TypeSpec compile/type generation, the dashboard typecheck and build, and bash -n + shellcheck on every maintained shell script.

Command Description
make check Every quality gate above (what CI runs)
make test Go unit tests only
make lint go vet, website Oxlint and shellcheck
make fmt / make fmt-check Format / verify formatting (Go + website)
make e2e Playwright end-to-end tests for website + dashboard lifecycle/retry/cleanup flows

See E2E_TESTING.md for what is automated in CI versus what requires a human running a manual runbook locally (OrbStack VM, WSL2, Multipass).


📋 Makefile Commands

The Makefile is split per sub-package under mk/ (go, spec, web, website, shell, cache, stack, vm). Run make help for the full, grouped list. Highlights:

Command Description
make install Bootstrap: install deps, build spec/UI/engine, install git hooks
make env Run interactive .env configuration wizard
make build Build the autoscaler and runner (arm64 + amd64) images
make build-vm-base Build golden OrbStack VM base image for near-instant VM spins
make start (or make up) Launch autoscaler, VM bridge, dashboard and caching proxies
make stop (or make down) Stop the autoscaler, proxies and active runners
make status (or make ps) Show autoscaler, proxies and active ephemeral runners
make logs / make logs-all Stream autoscaler / all-service logs
make dashboard Open the dashboard at http://localhost:49505
make check Run every quality gate
make install-hooks Install pre-commit and pre-push guards into .git/hooks/
make website-dev / make website-build Astro docs dev server / build and sync to docs/
make cache-smoke / make cache-size / make info Validate proxies / cache disk usage / total disk usage
make clean-caches / make clean-all Clear all caches / full reset
make help Show all available Makefile commands

⚙️ Configuration Reference (.env)

Variable Description Default
ACCESS_TOKEN GitHub Personal Access Token (PAT) with repo scope Required
OWNER GitHub username to auto-discover all owned repos None
REPOS Comma-separated list of target repos (owner/repo) None
ORG Organization mode: runners register at org scope and serve queued jobs from the org's active repos None
RUNNER_BACKEND Execution driver (auto, docker, orbstack-vm, wsl2, multipass) auto
AUTO_ROUTE_VM Automatically route browser/systemd/e2e jobs to VMs true
AUTO_DISCOVER_REPOS Automatically discover and monitor all user repos true
ACTIVE_REPO_DAYS Only monitor repos pushed in the last N days 60
RUNNER_ARCH Runner architectures to spawn (arm64, amd64, or both) both
PROXIES_ENABLED Enable apt-cacher, Verdaccio & Athens proxy registries for runners true
CACHE_ENABLED Enable persistent package/tool caching across runners true
MIN_RUNNERS Warm standby pool topped up after queued jobs are served (org scope in ORG mode, else round-robin across tracked repos; capped by MAX_RUNNERS; exempt from idle reaping) 0
MAX_RUNNERS Maximum concurrent runner instances 4
POLL_INTERVAL Queue check interval in seconds 10
DISCOVERY_INTERVAL Auto-discovery refresh interval in seconds 900
RATE_LIMIT_REFRESH_INTERVAL GitHub API quota refresh interval in seconds 60
ACTIONS_BILLING_REFRESH_INTERVAL Actions minutes refresh interval in seconds 300
GITLAB_URL Base URL for GitLab API (supports self-hosted instances like https://git.eckelmann.de) https://gitlab.com
GITLAB_TOKEN GitLab Personal / Group Access Token with api scope None
GITLAB_GROUP GitLab group to discover and monitor runners and queued jobs None
GITLAB_PROJECTS Comma-separated list of GitLab project paths to monitor None
PROVIDERS_CONFIG Optional path to a multi-instance providers YAML file None

✅ On-Prem Readiness Checklist

Use this checklist before announcing your RunZero host to teammates or external users.

  1. Start stack and bridge.
make restart
  1. Verify all cache proxies are reachable from both host and runner network.
make cache-smoke
  1. Confirm runner image startup summary shows private endpoints (not public defaults).
make logs

Look for lines showing Verdaccio/Athens/devpi/apt-cacher/kellnr connectivity.

  1. Ensure host Docker daemon uses the local Docker mirror for Docker-backend jobs.

Create or update /etc/docker/daemon.json:

{
   "registry-mirrors": ["http://localhost:49502"],
   "insecure-registries": ["localhost:49502"]
}

Then restart Docker and rerun:

make cache-smoke
  1. Validate real cache growth after one representative workflow run.
make cache-size

You should see non-zero growth in host cache and proxy volumes over repeated runs.


🤝 Contributing

Contributions are what make the open-source community an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

Please see CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.


📄 License

Distributed under the MIT License. See LICENSE for more information.

About

RunZero is the answer if action minutes run low!

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages