GitHub Actions — self-hosted runners
Every workflow that touches the lab runs on the lab’s own runners. GitHub-hosted runners cannot reach the tailnet, the directory, or either cluster — so for lab work they are not a fallback, they are a category error.
Part A — How it is set up
2026-11-01: lab-monorepo#102 closed; all ARC runners are on pro. The mini stack was removed, including its actions-runner-system namespace. Runner labels are unchanged.
- Controller and runner sets: ARC runs on pro; the mini stack is retired. The organisation-level scale sets retain their existing names and labels.
- Capacity: use the pro scale sets’ configured limits; the former mini cap is no longer a hosting constraint.
- Who depends on them: the docs pipeline
(cicd.md), the
ldap-group-applyworkflow that creates LDAP groups from git (identity.md), the per-app IAC pipelines of the signed supply chain, and the ServiceNow group-apply loop. Each would fail in a distinctive way on a hosted runner (network unreachable) — that failure mode is the tell that a workflow lost itsruns-onlabels. - Runner image quirks (learned once, recorded forever):
python3exists but there is no pip / ensurepip — workflows bootstrap pip explicitly or avoid it (stdlib-only scripts).- Node is not preinstalled — workflows that need it use
actions/setup-node(the docs workflow does). - Tooling is fetched as static binaries rather than installed
(a pinned
yqbinary is the standing example). - Non-interactive Docker on the tooling host cannot reach the
macOS keychain credential helper; builds that need anonymous
pulls set
DOCKER_CONFIG=/tmp/docker-nocreds(the docs image build does exactly this).
Part B — How to use it
-
Target the runners: every lab-touching job starts with
runs-on: mini-k8sIf a step must run in a container, the container runs on the runner pod — the
ldap-group-applyworkflow’spython:3.12-slimjob container is the working example. -
Check runner health:
kubectl get pods -Aon pro shows live ARC pods; the GitHub organisation’s Actions → Runners view shows the scale set’s registration. A job stuck at “Queued” is almost always one of: the configured capacity is busy, the labels drifted, or the controller is down — in that order. -
Debug a failed job: read the step log first; if the failure is a network error to a lab name, suspect the
runs-onlabels before the network. If it is a missing-tool error (pip,node,yq: command not found), it is the runner-image quirks above — fix the workflow, not the runner. -
Secrets in workflows: values come from GitHub Actions secrets (placed by a human or re-cut from Vault — the Harbor CI secret is the worked example, tools/harbor.md) or from Vault through a Secrets Operator tenant for in-cluster consumers. A credential typed into a workflow file is an incident, not a shortcut.