gh-runners
Cross-platform GitHub Actions self-hosted runner manager. One CLI to set up, manage, and tear down self-hosted runners across multiple orgs.
Self-hosted runners save real money on your GitHub Actions bill. GitHub-hosted runners charge per-minute and the costs add up fast, especially for Rust/Tauri builds where a single CI run can burn 30-60 minutes of billable time. With gh-runners, you run those same builds on your own hardware at zero marginal cost. A mid-range desktop running 10 parallel runners will pay for itself in weeks if you have active CI.
Built primarily for Rust/Tauri/Node CI but works for any workload. Linux and Windows supported. macOS PRs welcome.
Install
# From PyPI
uv tool install gh-runners
# Or pip
pip install gh-runners
# Verify
gh-runners --help
Quick Start
Linux
# 1. Clone and configure
git clone https://github.com/nazq/gh_runners.git
cd gh_runners
cp config.example.toml config.toml
# Edit config.toml with your org URL, runner count, etc.
# 2. Check prerequisites
gh-runners check-host
# 3. Install isolated toolchain (keeps runners separate from your dev tools)
gh-runners setup-toolchain
# 4. Setup runners (auto-fetches token via gh CLI, or pass --token)
gh-runners setup
Windows
# 1. Clone and configure
git clone https://github.com/nazq/gh_runners.git
cd gh_runners
copy config.example.toml config.toml
# Edit config.toml
# 2. Check prerequisites
gh-runners check-host
# 3. Verify globally installed tools match config versions
gh-runners setup-toolchain
# 4. Setup runners (as Administrator — installs as Windows Services)
gh-runners setup --token YOUR_TOKEN
Runners auto-start on reboot on both platforms.
Commands
All commands support --org <name> to target a specific organization.
| Command | Description |
|---|---|
gh-runners check-host |
Verify build toolchain prerequisites |
gh-runners list-packages |
List all available toolchain packages |
gh-runners setup-toolchain |
Install isolated toolchain (Linux) or verify versions (Windows) |
gh-runners setup |
Download, configure, install as services |
gh-runners status |
Show all runner service states and active jobs |
gh-runners start |
Start all runner services |
gh-runners stop |
Stop all runner services |
gh-runners restart |
Wait for jobs, clean work dirs, restart |
gh-runners restart --force |
Restart immediately (may interrupt jobs) |
gh-runners clean |
Clean _work directories (stop runners first) |
gh-runners logs <org> <N> |
Show last 50 log lines for runner N |
gh-runners remove |
Unregister from GitHub, remove services |
setup and remove accept an optional --token TOKEN. If omitted, a registration token is fetched automatically via the gh CLI.
Configuration
Copy config.example.toml to config.toml:
[runner_version]
version = "2.331.0"
[timeouts]
job_wait_seconds = 3600
poll_interval = 10
# Pluggable toolchain — each package gets its own sub-table
[toolchain]
packages = ["rust", "node", "cargo-tools"]
[toolchain.rust]
version = "1.88.0"
[toolchain.node]
version = "22.14.0"
[toolchain.cargo-tools]
crates = "cargo-llvm-cov just tauri-cli"
# Add as many orgs as you need
[[org]]
name = "MyOrg"
url = "https://github.com/MyOrg"
runner_group = "Default"
runner_count = 4
name_prefix = "runner"
service_prefix = "gh-runner-myorg"
extra_labels = ""
Key settings
- runner_count: Parallel runners per org. Match to your CPU cores / build needs.
- name_prefix: Shows in GitHub UI as
prefix-1,prefix-2, etc. - service_prefix: Systemd service name prefix (Linux) or Windows Service name.
- base_dir: Where runner binaries live. Defaults to
~/.gh-runners/<org_name>. - extra_labels: Additional labels beyond the automatic
self-hosted,Linux/Windows,X64.
Toolchain packages
Each package is a TOML sub-table under [toolchain]. The packages array controls which ones are installed. Run gh-runners list-packages to see all available packages and their supported architectures.
Built-in packages: rust, node, cargo-tools, go, pnpm, bun.
Note: Some cargo crates (especially
tauri-cli) take 15+ minutes to compile on first install. This is normal for Rust — subsequent installs are cached.
Architecture
Linux: Toolchain Isolation
Runners use an isolated shared toolchain (~/.gh-runners/shared-toolchain/) with their own RUSTUP_HOME, CARGO_HOME, and Node.js — completely separate from your personal ~/.cargo and ~/.nvm. The systemd service files load each runner's .env file via EnvironmentFile= so runners never see your dev tools.
~/.gh-runners/
├── shared-toolchain/ # Isolated Rust + Node + whatever you configure
│ ├── .rustup/
│ ├── .cargo/
│ └── node/
├── MyOrg/
│ ├── runner-1/ # Runner installation + _work/
│ ├── runner-2/
│ └── ...
└── AnotherOrg/
└── ...
Windows
On Windows, gh-runners setup-toolchain verifies that globally installed tool versions match your config. Runners use whatever Rust/Node is on the system PATH. Each runner is a native Windows Service via GitHub's built-in svc.cmd.
Platform Detection
Auto-detects OS and CPU architecture:
- OS: Linux, Windows (macOS ready for PRs)
- Arch: x64, arm64, arm
Prerequisites
Linux
- Python 3.11+ with uv
- git, gcc, curl
- Then run
gh-runners setup-toolchainfor Rust + Node
Windows
- Python 3.11+ with uv
- Git for Windows (
winget install Git.Git) - Visual Studio Build Tools with C++ workload
- Rust (
winget install Rustlang.Rustup) - Node.js LTS (
winget install OpenJS.NodeJS.LTS)
Run gh-runners check-host to verify everything is present.
Usage in GitHub Actions
jobs:
build:
runs-on: [self-hosted, Linux, X64]
steps:
- uses: actions/checkout@v4
- run: cargo build --release
# Target a specific runner by its name label
build-specific:
runs-on: [self-hosted, Linux, X64, gh-runner-myorg-1]
steps:
- uses: actions/checkout@v4
Development
just check # Run all checks (lint + typecheck)
just lint # Ruff lint + format check
just fix # Auto-fix lint issues and format
just typecheck # mypy --strict
just run status # Run any gh-runners command via uv
Contributing
PRs welcome, especially for macOS support. Run just check before submitting — it must pass clean.
Adding a new package
The toolchain system is pluggable. To add a new package (e.g. deno):
- Add an install function in
gh_runners/packages.py:
def _install_deno(tc_dir: Path, arch: str, cfg: dict[str, Any]) -> None:
version: str = cfg.get("version", "2.0.0")
# Download and extract into tc_dir / "deno" or similar
# The cfg dict is the full TOML sub-table, so any keys
# you define under [toolchain.deno] are available here.
...
- Register it in the
PACKAGESdict (same file):
PACKAGES: dict[str, Package] = {
# ... existing packages ...
"deno": Package(
name="deno",
description="Deno JavaScript/TypeScript runtime",
install_fn=_install_deno,
supported_archs={"x64", "arm64"},
default_version="2.0.0",
host_checks=[
HostCheck(
name="deno",
cmd=["deno", "--version"],
parse=lambda out: out.splitlines()[0].split()[1],
why="Deno runtime",
),
],
),
}
- Add a path helper if the package installs to its own directory:
def deno_home(tc_dir: Path) -> Path:
return tc_dir / "deno"
Then add it to toolchain_env() in toolchain.py so it appears in the runner PATH.
- Update
config.example.tomlwith a commented-out example.
That's it. No config schema changes needed — users just add "deno" to their packages list and create a [toolchain.deno] sub-table with whatever keys your install function reads.
Troubleshooting
Runner shows offline in GitHub
gh-runners status
gh-runners logs MyOrg 1
gh-runners restart
Registration token expired
Tokens expire after 1 hour. If you have gh CLI authenticated, gh-runners setup and gh-runners remove fetch tokens automatically. Otherwise:
gh api -X POST orgs/YOUR_ORG/actions/runners/registration-token --jq .token
gh-runners setup --token TOKEN
Disk space
Rust target/ dirs and node_modules grow fast:
gh-runners stop
gh-runners clean
gh-runners start
Re-register runners
gh-runners remove
gh-runners setup
Release files for gh-runners 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gh_runners-1.2.0.tar.gz | 27.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gh_runners-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.8 kB
Release files / gh_runners-1.2.0.tar.gz
| Download URL | gh_runners-1.2.0.tar.gz |
|---|---|
| Size | 27.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
499df74a42d400e777132a0be1417d6a6be84059e92814fb56c9bb60091d8d27
|
|
BLAKE2b-256 checksum How to use checksums |
1a5da40caae0890bb04dd5e73eb94e70e2798cb73e676de13acf061bd7de7f13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / gh_runners-1.2.0-py3-none-any.whl
| Download URL | gh_runners-1.2.0-py3-none-any.whl |
|---|---|
| Size | 29.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9d5b81bc72d4c9bfa563254be24f99480e430d0ac4344a4a6ab52bf5d9263228
|
|
BLAKE2b-256 checksum How to use checksums |
5666d84d72d6cadaba686c0b551332f8de3c52ef294aa167984b6333cdd8f7d6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|