Skip to content

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-0104 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.