Skip to content

Why kinhin ​

Self-hosted CI runners do one job at a time. kinhin exists to give your Mac real concurrency.

← Home

The problem ​

A self-hosted runner is built to do one thing at a time. Register a single runner with your CI platform and your Mac builds one job while every other job waits in the queue. Each platform draws the line a little differently, but none of them gives you concurrency for free:

PlatformHow concurrency is limitedSource
GitHub ActionsAn ephemeral runner is assigned exactly one job; GitHub recommends autoscaling a fleet of them for clean, concurrent builds.Self-hosted runners, Ephemeral runners & autoscaling
GitLab CI/CDJob concurrency is set by hand with concurrent and per-runner limit in config.toml.Advanced configuration
Gitea / Forgejo Actionsact_runner runs capacity: 1 task at a time unless you raise it.act_runner config example

The details differ (a per-runner setting on one platform, an organization or plan limit on another), but the shape is the same: to run jobs in parallel you need many runners, and someone has to create, register, clean up, and retire them.

Why the usual workarounds fall short ​

  • Several runners on one OS. Jobs share one filesystem, one set of ports, and one toolchain cache, so state leaks between builds and parallel jobs collide.
  • Long-lived VMs per runner. Isolation improves, but idle VMs hold RAM and disk all day and every platform needs its own bespoke setup.
  • Hand-rolled autoscalers. Each forge has its own queue API and registration flow, so the glue is rewritten per platform.
  • macOS adds a hard ceiling. Apple's license and Virtualization framework allow only two macOS guest VMs at once on a Mac, so "one VM per runner" tops out at two. Linux guests are not subject to that limit (Eclectic Light, Apple Developer Forums).

Weighing kinhin against another tool, such as Woodpecker CI, Actions Runner Controller, Orchard or a hosted Mac service? See Compare kinhin to other CI options.

What kinhin does instead ​

kinhin watches the queued jobs on every forge you configure and scales a fleet of ephemeral runners to match. A queued job gets a fresh linked-clone VM (or container), runs, and the runner is deregistered and the instance deleted. Nothing carries over between jobs, and nothing sits idle waiting for work.

  • One engine, every forge. The same fleet serves GitHub, GitLab, Gitea and Forgejo from one pool (see Forge support); Buildkite, Azure Pipelines and Bitbucket are planned.
  • Concurrency without the VM ceiling. runners_per_vm runs several runner processes inside one Tart VM, and Linux jobs run in containers, microVMs, or full Tart Linux VMs, so you are not held to two simultaneous jobs.
  • Never overcommits your Mac. The cap is computed from your cores, RAM, and free disk after reserving host headroom (see How scaling works).