GitHub Actions self-hosted runner manager for Linux, Windows, and more
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gh_runners-1.1.0.tar.gz.
File metadata
- Download URL: gh_runners-1.1.0.tar.gz
- Upload date:
- Size: 24.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3f74e9dcd42a8ac2ba4573ce385b316e60c26510b1aec14db22b2650b74312a
|
|
| MD5 |
19b83ceb1f68b8727384d25b9083ada5
|
|
| BLAKE2b-256 |
6a366be24d3b40810fa616e85012a5a7f8702063224d8b1f5068b1caaaa5026d
|
File details
Details for the file gh_runners-1.1.0-py3-none-any.whl.
File metadata
- Download URL: gh_runners-1.1.0-py3-none-any.whl
- Upload date:
- Size: 26.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27314d8dea4e2f2000293a0ff149a9920d2d270236508e2dc6a32cce75208a16
|
|
| MD5 |
c15aa1d523cfd3b4ec5f63b0c2c310e9
|
|
| BLAKE2b-256 |
3fd0fec0b19310f65e79f8e6954e3366501318851d1661bfcb05ddeaaf35b3d5
|