GitHub Actions — self-hosted runners (homelab)¶
[!WARNING] This document is a historical record of the transitional WSL fleet, which was stopped, disabled, and unregistered on 2026-07-27. Do not run its registration commands. The authoritative procedure is GitHub Actions runner fleet.
Homelab CI uses repo-scoped self-hosted runners split by trust zone.
GitHub-hosted ubuntu-latest was blocked by account billing (2026-06-26), so
pull requests run on an isolated self-hosted pool instead of the trusted
homelab CI/LAN pools.
Runner admin: homelab → Settings → Actions → Runners
Same pattern as tiktooker
(tiktok-ci) and dnd_session_parser
(dnd-session-parser) on WSL.
Pools¶
| Label | Host | Use when |
|---|---|---|
homelab-pr |
Isolated WSL/VM/container runner | Untrusted pull_request validation only |
homelab-ci |
WSL on dev PC (CaptainKangapoo) |
Trusted push, docs deploy, Tailscale ACL, scheduled scans |
homelab-lan |
infra-services (192.168.6.17) |
Trusted Komodo webhook, UniFi/Proxmox scans — needs LAN DNS/IPs |
dependabot |
WSL runner 04 (also carries homelab-ci) |
Trusted Dependabot push/scheduled CI pool |
Workflows use explicit labels so homelab jobs never land on tiktok/dnd runners (those are registered to other repos).
runs-on: [self-hosted, Linux, X64, homelab-pr]
# or
runs-on: [self-hosted, Linux, X64, homelab-ci]
# or
runs-on: [self-hosted, Linux, X64, homelab-lan]
# Mixed PR/push workflows:
runs-on: ${{ fromJSON(github.event_name == 'pull_request' && '["self-hosted","Linux","X64","homelab-pr"]' || '["self-hosted","Linux","X64","homelab-ci"]') }}
Retired inventory¶
| Name | Pool | Status |
|---|---|---|
infra-services |
homelab-lan |
Retired 2026-07-27 |
windows-CaptainKangapoo-homelab-pr-runner-01 |
homelab-pr |
Retired 2026-07-27 |
windows-CaptainKangapoo-homelab-runner-01–04 |
homelab-ci / dependabot |
Retired 2026-07-27 |
Check the replacement managed fleet:
gh api repos/spadoople/homelab/actions/runners --jq '.runners[] | {name, status, labels: [.labels[].name]}'
Historical: register homelab-pr (isolated PR validation)¶
homelab-pr runners execute untrusted pull-request code. They must be treated
as hostile-by-default and must not share the trusted CI or LAN runner boundary.
Required isolation:
- No SOPS age private keys.
- No SSH deploy keys or host aliases for homelab infrastructure.
- No Docker socket access.
- No Tailscale session or advertised routes.
- No LAN reachability to RFC1918 homelab ranges where host firewalling allows it.
- Outbound HTTPS only for GitHub, PyPI, npm, GitHub release downloads, and other public package registries needed by CI.
- Disposable or routinely rebuilt workspace. Ephemeral runners are preferred.
The registration command was removed with the legacy installer. Historical label validation used:
gh api repos/spadoople/homelab/actions/runners \
--jq '.runners[] | select(.labels[].name=="homelab-pr") | {name, status, labels: [.labels[].name]}'
Validate isolation from the runner host:
# Should fail from the PR runner host.
timeout 5 nc -z 192.168.6.17 22
timeout 5 nc -z 192.168.6.17 443
timeout 5 nc -z 100.100.100.100 53
Do not add homelab-ci, homelab-lan, dependabot, or service-specific
labels to a PR runner.
Register homelab-lan (infra-services)¶
Already installed at /home/someone/actions-runner (2026-06-26). Add the custom
label if the runner only has defaults:
# From dev machine (gh auth)
gh api --method PUT repos/spadoople/homelab/actions/runners/RUNNER_ID/labels \
-f "labels[]=homelab-lan"
Or re-register with labels:
ssh infra-services
cd ~/actions-runner
./config.sh remove
# New token: GitHub → homelab → Settings → Actions → Runners → New self-hosted runner
./config.sh --url https://github.com/spadoople/homelab \
--token TOKEN \
--name infra-services \
--labels homelab-lan \
--unattended
sudo ./svc.sh install someone
sudo ./svc.sh start
Retired service: actions.runner.notarealemail-homelab.infra-services.service
Prereqs on host: git, Python 3.12+, uv optional; LAN DNS for
*.infra.realemail.app, reachability to UDM (192.168.1.1) and Proxmox as needed.
Historical: register homelab-ci (WSL)¶
Mirror the tiktok/dnd WSL layout — one or more runners with the homelab-ci
label only (do not reuse tiktok-ci / dnd-session-parser registrations; those
belong to other repos).
The legacy installer was removed when the WSL fleet was retired. The following manual example is retained only to explain old runner records and logs.
Manual one-off (token from GitHub → homelab → Settings → Actions → Runners):
DIR=~/actions-runner-homelab-ci-01
mkdir -p "$DIR" && cd "$DIR"
curl -fsSL -o runner.tar.gz \
https://github.com/actions/runner/releases/download/v2.335.1/actions-runner-linux-x64-2.335.1.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/spadoople/homelab --token TOKEN \
--name windows-CaptainKangapoo-homelab-runner-01 --labels homelab-ci --unattended
sudo ./svc.sh install someone && sudo ./svc.sh start
Prereqs: git, Python 3.12/3.13, Node 22, npm, Docker optional. Runners
need outbound HTTPS (PyPI, npm, Cloudflare API for docs deploy).
Parallelism: lint.yml runs nine jobs — two or three homelab-ci runners
avoid long queues. Dependabot PRs target runner 04 (homelab-ci + dependabot)
so dependency bumps do not starve human PRs.
Dependabot¶
Config: .github/dependabot.yml
— npm (root), pip (pyproject.toml), GitHub Actions, and docker-compose per
services/* stack.
Dependabot no longer receives a privileged PR pool. Pull-request events,
including Dependabot PRs, run on homelab-pr. The dependabot label remains
available for trusted non-PR Dependabot maintenance if needed.
Runner 04 was disabled and unregistered with the rest of the legacy fleet.
Security workflows¶
Ported from dnd_session_parser.
Pull-request executions run on homelab-pr; trusted push, scheduled, and manual
executions stay on homelab-ci.
| Workflow | Tools | Triggers |
|---|---|---|
| Secrets scan | Gitleaks + TruffleHog OSS | PR, push, weekly Tue 03:30 |
| Dependency Security | pip-audit (uv.lock) + npm audit |
PR (path-filtered), push, weekly Mon 07:00 |
| Semgrep OSS | p/ci ERROR gate |
PR, push, weekly Mon 07:30 |
Gitleaks config: .gitleaks.toml allowlists SOPS ciphertext and repo placeholders. pip-audit baseline: .github/pip-audit-ignore.txt.
No new GitHub secrets are required — scans use GITHUB_TOKEN (Gitleaks) or run offline.
Optional: add DISCORD_WEBHOOK and a dependabot-discord.yml-style workflow if you want PR
notifications like dnd.
Branch protection (GitHub UI): Settings → Branches → main → require status checks:
Gitleaks, TruffleHog OSS, Semgrep OSS (high-confidence gate), and the relevant
Dependency Security jobs once the first runs are green.
Workflow → pool map¶
| Workflow | Pool |
|---|---|
| lint.yml | homelab-pr for PRs; homelab-ci for push |
| secrets-scan.yml | homelab-pr for PRs; homelab-ci for push/schedule/manual |
| dependency-security.yml | homelab-pr for PR gates; homelab-ci for scheduled/manual advisory scan |
| semgrep.yml | homelab-pr for PRs; homelab-ci for push/schedule/manual |
| docs.yml | homelab-ci |
| tailscale-acl.yml | homelab-ci |
| komodo-deploy.yml | homelab-lan |
| network-scan.yml | homelab-lan |
| proxmox-scan.yml | homelab-lan |
| unifi-fix-airplay.yml | homelab-lan |
Troubleshooting¶
Job queued forever¶
No online runner with the requested label. Register a runner or fix labels:
gh api repos/spadoople/homelab/actions/runners --jq '.runners[] | select(.status==\"online\") | .labels[].name' | sort -u
Wrong pool picked up job¶
Labels are wrong on the runner. LAN jobs must not carry homelab-ci only;
CI runners must not be the sole homelab-lan host unless you accept LAN
workloads on WSL (not recommended — no infra DNS).
Runner offline after reboot¶
# Managed LAN runner
ssh infra-services 'sudo systemctl status github-runner@homelab-lan-01'
# Managed Hyper-V hosts use github-runner@<instance>.service.
Related¶
- Komodo GitHub webhook relay — why
homelab-lanexists - Cloudflare Pages (docs deploy) —
homelab-ci+ CF secrets - GitHub: Managing self-hosted runners