Runner Routing Strategy¶
Rule: Match every job's
runs-onto the workload's resource, network, and cost profile. Never default all jobs toubuntu-latestwhen self-hosted capacity exists.
Decision Matrix¶
| Workload characteristic | Recommended runner |
|---|---|
| Needs Azure/cloud access (deploy, Packer, Key Vault) | Self-hosted (has managed identity) |
| Heavy compute (image builds, large compiles) | Self-hosted (dedicated capacity) |
| Quick CI gate (<30 s typical) | GitHub-hosted (fast cold-start) |
| Requires CodeQL or GitHub-native tooling | GitHub-hosted |
| Security-sensitive (secret access, regulated data) | Self-hosted (controlled environment) |
| PR validation (needs fast feedback) | GitHub-hosted or hybrid |
| Untrusted code from external forks | GitHub-hosted only |
Routing Patterns¶
Pattern 1: Direct routing to a runner group¶
Use when the job always requires self-hosted resources (deployment, image build, Key Vault access).
jobs:
deploy:
name: Deploy to Azure
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64]
Pattern 2: Configurable routing via repository variable¶
Use when teams need to toggle between runner types without changing workflow code. Set USE_SELF_HOSTED to 'true' in repository or environment variables to route to self-hosted runners.
jobs:
build:
name: Build
runs-on: >-
${{
vars.USE_SELF_HOSTED == 'true'
&& fromJson('{"group":"shared-build-agents","labels":["self-hosted","linux","x64"]}')
|| 'ubuntu-latest'
}}
Pattern 3: Fast-fail timeout for self-hosted¶
Use when self-hosted is preferred but the workflow must not block indefinitely if the pool is scaled to zero.
jobs:
build:
name: Build
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64]
timeout-minutes: 10 # fail fast if no runner picks up the job
Pattern 4: Hybrid pipeline — route each job by workload type¶
Use for pipelines that include both lightweight CI gates and heavyweight deployment jobs.
jobs:
lint:
name: Lint
runs-on: ubuntu-latest # fast, cheap, no network requirements
test:
name: Test
runs-on: ubuntu-latest # stateless, no private network needed
needs: [lint]
build-image:
name: Build Container Image
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64] # dedicated CPU / cache
needs: [test]
deploy:
name: Deploy to Azure
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64] # managed identity required
needs: [build-image]
if: github.ref == 'refs/heads/main'
Rationale¶
| Criterion | GitHub-hosted | Self-hosted |
|---|---|---|
| Cold-start latency | ~10–30 s | Varies (0 s if warm, minutes if scaled to zero) |
| Managed identity / private network | ❌ No | ✅ Yes |
| Runner minutes cost | Counts against plan quota | Infrastructure cost only |
| Maintenance burden | ❌ None | ✅ Team-managed |
| Isolation from other workloads | ✅ Ephemeral VM per job | Depends on runner pool config |
| GitHub-native tooling (CodeQL, etc.) | ✅ Pre-installed | May require manual setup |
Anti-Patterns¶
All jobs on ubuntu-latest when self-hosted runners exist¶
# ❌ BAD — ignores self-hosted capacity; incurs GitHub-hosted minutes
jobs:
deploy:
runs-on: ubuntu-latest # cannot reach Azure Key Vault; will fail
Self-hosted runners for trivial jobs¶
# ❌ BAD — wastes dedicated capacity on a 5-second echo step
jobs:
greet:
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64]
steps:
- run: echo "Hello"
No timeout on self-hosted jobs¶
# ❌ BAD — job waits indefinitely when pool is scaled to zero
jobs:
build:
runs-on:
group: shared-build-agents
labels: [self-hosted, linux, x64]
# missing timeout-minutes
Mixing sensitive and untrusted workloads on the same runner group¶
# ❌ BAD — external fork PR runs on the same runner that handles secrets
on:
pull_request_target: # runs in privileged context
jobs:
ci:
runs-on:
group: shared-build-agents # has access to org secrets
labels: [self-hosted]
External contributor PRs must run on GitHub-hosted runners or an isolated, unprivileged runner group with no secret access.
Quick Reference¶
| Job type | Recommended runs-on |
|---|---|
| Lint / unit test / PR gate | ubuntu-latest |
| CodeQL / dependency scan | ubuntu-latest |
| Container image build (heavy) | group: shared-build-agents + [self-hosted, linux, x64] |
| Deploy (Azure / AWS / GCP) | group: shared-build-agents + [self-hosted, linux, x64] |
| Packer / VM image build | group: shared-build-agents + [self-hosted, linux, x64] |
| Key Vault / secrets retrieval | group: shared-build-agents + [self-hosted, linux, x64] |
| External fork / untrusted PR | ubuntu-latest only |
Related Guardrails¶
- CI/CD Concurrency Settings — prevent duplicate runs and stuck queues
- OIDC Federation — prefer OIDC over long-lived secrets on self-hosted runners
- Secrets in Workflows — handle secrets safely regardless of runner type