Git, CI/CD & GitOpsXL · RunnersRunners
Runner labels and selection — matching workflows to runners, and the trust model
What you'll learn
- Explain how labels work: strings attached to runners, matched by workflow runs-on
- Read and write a runs-on label expression correctly
- Identify the four label trust classes: hosted defaults, self-hosted untrusted, self-hosted prod, GPU/specialised
- Recognise the default-routing risk: self-hosted runners with no label receive every workflow
- Design a label taxonomy that maps cleanly onto the team pool structure
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A runner label is a string attached to a runner. A workflow’s
runs-on field is a list of strings. The forge matches the
workflow to a runner when every label in runs-on is present
on the runner. The match is exact, all-or-nothing: a runner
missing one label does not take it.
Labels are how a workflow says “I need a Linux x64 host with production secrets” and a runner says “I have those properties”. They are also how the team keeps the wrong job from landing on the wrong host.
The mechanics
flowchart LR
W["Workflow\nruns-on: [self-hosted,\nlinux, x64, prod]"] --> M["Forge matches\nall labels"]
R1["Runner\n[self-hosted, linux,\nx64, prod]"] --> M
R2["Runner\n[self-hosted, linux,\narm64]"] --> M
R3["Runner\n[self-hosted, linux,\nx64, gpu]"] --> M
M -->|"match"| R1
M -->|"no 'prod'"| R2
M -->|"no 'gpu' label\nneeded"| R3
The match: the workflow wants every label. The runner has
extra labels and that’s fine — a runner tagged
[self-hosted, linux, x64, prod, gpu] matches a workflow
asking for [self-hosted, linux, x64, prod]. The runner
having more than the workflow needs does not disqualify it.
# A workflow that wants a specific runner class
jobs:
build:
runs-on: [self-hosted, linux, x64, prod]
steps:
- uses: actions/checkout@v4
Built-in hosted labels
GitHub-hosted runners come with a fixed label set:
ubuntu-latest,ubuntu-22.04,ubuntu-20.04windows-latest,windows-2022macos-latest,macos-14,macos-13
The team chooses among them; the team cannot add to them.
The labels change meaning over time — ubuntu-latest today
is ubuntu-22.04 plus patches, and will become ubuntu-24.04
when GitHub publishes that image generation.
Self-hosted labels
Self-hosted runners are labelled at registration:
# Register with three labels
./config.sh --url "$REPO_URL" --token "$REGISTRATION_TOKEN" \
--labels "linux,x64,prod"
flowchart LR
A["config.sh\n--labels linux,x64,prod"] --> B[".runner file"]
B --> C["Forge sees labels:\nself-hosted, linux, x64, prod"]
The team chooses the labels. There is no schema; any string
works. The self-hosted label is implicit — every
self-hosted runner has it — but workflows conventionally
include it for clarity.
The default-routing trap
flowchart TB
R["Self-hosted runner\n(no labels)"] -->|"matches\nanything"| J1["Trusted PR job"]
R --> J2["Fork PR job"]
R --> J3["Random branch push"]
A self-hosted runner registered with no labels matches
every runs-on expression that does not explicitly
exclude it. Including fork pull requests from any
contributor. Including branches the team has never seen.
This is the canonical misconfiguration. An engineer adds a self-hosted runner for “internal builds”, forgets to label it, and discovers six months later that fork-PR jobs have been running on the same host as production builds. The trust model has been silently violated.
A label taxonomy
The labels a team picks encode the team’s pool structure. A clean taxonomy follows:
flowchart TB
subgraph SH["Self-hosted"]
P["prod"]
S["staging"]
U["untrusted\n(fork PRs)"]
G["gpu"]
A["arm64"]
end
subgraph H["Hosted"]
UB["ubuntu-latest"]
ML["macos-latest"]
end
prod/staging/untrusted— trust class. The workflow picks the pool by picking the trust class.linux/windows/macos— OS family. Most workflows need this.x64/arm64— architecture. Workflows building multi-arch images need this.gpu— specialised hardware. A workflow that needs CUDA asks forgpu.- Per-team labels —
team-frontend,team-data, etc., to carve out capacity for high-priority teams.
Workflows that want a production Linux x64 runner:
runs-on: [self-hosted, linux, x64, prod]
Workflows that want a fork-PR sandbox:
runs-on: [self-hosted, linux, untrusted]
The same workflow, two pools, separated by label.
Labels are a trust model
flowchart LR
W["Workflow"] -->|"runs-on: [untrusted]"| U["Untrusted pool\n(no secrets,\nno IAM)"]
W2["Workflow"] -->|"runs-on: [prod]"| P["Prod pool\n(full secrets,\nIAM, VPC)"]
The label expression is not just a routing hint. It is a statement about which trust class the workflow belongs to, which secrets the runner mounts, and which network the runner can reach. A misconfigured label can land a production job on an untrusted pool (the workflow loses its secrets), or a fork-PR job on a production pool (the runner loses its isolation).
Production discipline
- Every self-hosted runner has at least one label. No bare runners in production.
- Every workflow specifies
runs-onexplicitly. No defaulting to “any available runner”. - Labels reflect the trust model. A runner pool with
production secrets is labelled
prod; a runner pool with no secrets is labelleduntrusted. The label is the trust class. - Document the label taxonomy. New engineers need to know what labels exist and how to combine them.
- Audit label drift. A runner that was supposed to be
prodand is nowprod,gpuis a different runner for some workflows; the team should know.
Cross-course references
- Linux for Production Sysadmins - Part XXIV (ImmutInf) covers the host-tagging patterns that map onto runner labels.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers the pool-architecture patterns that labels express.
- Kubernetes for Production Sysadmins - Parts XXXII-XXXV (RBAC) cover the trust-model patterns that runner labels embody.
Quiz
Knowledge check · 4 questions
Q1. A self-hosted runner is registered with `config.sh --labels ""` (no labels). Which workflows can the forge send to it?
Q2. A runner with labels `[self-hosted, linux, x64, prod, gpu]` can match a workflow asking for `[self-hosted, linux, x64, prod]`.
Q3. Name three categories of runner labels and what each category expresses.
Q4. Design a runner label taxonomy for a team that needs to separate production builds from fork-PR builds from GPU builds, and identify the misconfiguration to avoid.
Team T runs three workflows: `prod-build` (production CI, needs AWS IAM), `fork-pr` (external contributions, no IAM), and `ml-train` (GPU training, needs CUDA). They use self-hosted runners for all three.
Passing score: 75%. Answers are checked in this browser.