GitHub Actions runner controller for macOS that keeps a target number of runners per group, favors ephemeral runners, and cleans up aggressively to avoid zombie workdirs or GitHub “offline” entries.
- Features
- Architecture
- Requirements
- Quickstart
- Configuration
- Commands
- Paths and Files
- Runner Lifecycle and Cleanup
- Operational Notes
- Troubleshooting
- Development
- Limitations
- self-hosted runner management without containers.
- Group-based desired state: name, count, labels, ephemeral flag, per-group workdir/version overrides.
- Daemon with reconciliation loop that reacts to runner exits and also runs on a periodic safety interval.
- SIGHUP-driven config reload (
ghr apply) without restarting the daemon. - Aggressive cleanup of stale workdirs, processes, and GitHub runner registrations that match configured group prefixes.
.env-based secrets; YAML config for desired state; defaults for paths/version.
High-level design, data paths, and control flow are documented in docs/ARCHITECTURE.md. Read it for package responsibilities and lifecycle details.
- macOS with
bashavailable. - Go 1.21+ to build (module targets Go 1.24.4).
- Network access to
github.comto download runner binaries and fetch registration tokens. - GitHub PAT with runner registration permissions exported as
GITHUB_TOKEN(orGITHUB_PAT) via.env.
git clone <this-repo>
cd gh-runners-tool
cp config.example.yaml config.yaml
cp env.example .env # edit with your PAT
go build ./cmd/ghr
# launch daemon (foreground)
./ghr daemon --config config.yaml --interval 15s.env is mandatory for authentication and stays out of version control.
GITHUB_TOKEN=YOUR_GITHUB_PAT_WITH_RUNNER_PERMS
# or
GITHUB_PAT=YOUR_GITHUB_PAT_WITH_RUNNER_PERMS
Security: keep .env in .gitignore and prefer a secrets manager for production.
Define the desired state for runners.
github:
scope: org # org | repo
owner: your-org
# repo: your-repo # required when scope=repo
defaults:
workdir_base: /var/lib/ghr/groups
cache_dir: /var/lib/ghr/cache
version: latest # or a pinned runner version
groups:
- name: deploy-api
count: 10
ephemeral: true
labels: [deploy, macos]
# workdir_base: /custom/path/deploy-api # optional override
# version: 2.319.1 # optional overrideValidation rules:
github.scopemust beorgorrepo;github.owneris required;github.reporequired whenscope=repo.- At least one group is required;
countmust be>= 0;namemust be set. - Defaults fill missing
workdir_base,cache_dir, andversion. Groups inherit defaults when not overridden.
ghr daemonStarts the controller. Flags:--config(defaultconfig.yaml),--interval(default15s). Writes a pid file and reloads config onSIGHUP.ghr applyValidates config and sendsSIGHUPto the running daemon for zero-downtime reload.ghr statusPrints the daemon pid if the pid file exists.ghr purgeDeletes all self-hosted runners for the configured scope, waiting for busy runners to go idle. Flags:--timeout(default5m),--interval(default5s).
Examples:
./ghr daemon --config config.yaml --interval 10s
./ghr apply --config config.yaml
./ghr status
./ghr purge --config config.yaml --timeout 10m --interval 10s- Workdirs:
/var/lib/ghr/groups/<group>/<id>by default; per-group overrides supported. - Cache:
/var/lib/ghr/cacheby default (shared runner bits). - State / pid file:
/var/lib/ghr/state/daemon.pid; falls back to$HOME/.local/state/ghrif/var/libis not writable.
- Resolve runner version (
latestor pinned), download/cache if missing. - Copy cached bits to a fresh per-runner workdir; run
config.sh --unattendedwith labels and optional--ephemeral; startrun.sh. - Reconciler observes exits and recreates ephemeral runners to maintain target counts; also runs on the configured interval.
- On startup, daemon kills stray runner processes found in configured workdir bases, then removes workdirs.
- On shutdown, daemon stops runners, deregisters them from GitHub, and removes workdirs.
- Startup and shutdown include GitHub cleanup of runner registrations with names prefixed by configured group names.
- Keep the daemon running (foreground or under a supervisor like
launchd); stopping it stops runners. - Reload configuration with
ghr apply(SIGHUP); no restart required. - Without containers, jobs run directly on the host. Use dedicated machines you trust and ensure filesystem permissions allow runner cleanup.
GITHUB_TOKEN (or GITHUB_PAT) is required→ ensure.envis present and exported in the environment that launches the daemon.- Permission denied on
/var/lib/ghr/...→ overrideworkdir_base,cache_dir, andstateto user-writable paths. - Runners remain “offline” in GitHub → run
ghr purgeto delete registrations; daemon also cleans prefixed registrations on startup/shutdown. - Download/registration errors → inspect daemon stdout logs; verify network access to
github.com.
- Build:
go build ./cmd/ghr - Lint/format: use
gofmtand standard Go tooling. - Tests:
go test ./...(add tests as features evolve).
- macOS only; no container isolation; Linux not supported yet.
- No HTTP health/metrics endpoint (planned later).
- Persistent groups restart on daemon restart; existing jobs may be terminated by cleanup policy.