Hive
Runners: opt-in run-scoped labels to prevent runner stealing
GitHub issue · Open
Summary
Offer customers an opt-in way to pin a runner to their own workflow run, so a runner Tuist starts for their job cannot be taken by a different job. This is the mitigation RunsOn documents for “runner stealing”, adapted to our label scheme.
Background
GitHub matches a job to a runner when the runner’s labels are a superset of the job’s runs-on. Every job asking for tuist-macos is therefore eligible for every runner we register with that label. When several jobs queue together, the runner we started for job X routinely runs job Y instead. RunsOn calls this runner stealing: “the runner started for your workflow job has been stolen by another workflow job.”
This is inherent to the label-matching model, not a Tuist defect — any provider using a single static label inherits it (Depot’s depot-ubuntu-24.04 and Blacksmith’s blacksmith-4vcpu-ubuntu-2204 have the same shape). #11862 tracks making the server correct in the presence of stealing (attribution, capacity accounting, reaping). This issue is about letting customers avoid it.
The only lever that can prevent stealing lives on the job side: the runner must be restricted by what the job asks for, and runs-on is authored by the customer. Adding extra labels to our runners cannot help — supersets match. So the customer has to interpolate something unique into runs-on, and our label parsing has to accept it.
Proposal
Accept an optional scope token in the dispatch label, e.g.:
# today — unchanged, still the default and still steal-able
runs-on: tuist-macos
# opt-in: only this workflow run's jobs can use the runner
runs-on: tuist-macos-${{ github.run_id }}
# opt-in: matrix-safe — run_id is shared across matrix legs
runs-on: tuist-macos-${{ github.run_id }}-${{ strategy.job-index }}
The server strips the scope token when resolving (account, requested-label) → profile → pool, and stamps the full label (including the token) on the JIT registration. GitHub then only matches that runner to jobs carrying the same token.
Why it’s worth doing
- Deterministic assignment — the runner we started for a job runs that job. Queue-time predictability, and the job detail page stops being a lottery.
- Warm cache targeting — a runner prepared for a specific run actually gets that run. Today any per-claim preparation can land on someone else’s job.
- Trust becomes safe for opt-in users, for free. Fork-ness is a property of the workflow run, not the job (
job_trusted?resolvesget_workflow_run(run_id)and compares head vs base). Every job in a run shares its verdict — so with a run-scoped label, stealing within a run cannot cross a trust boundary. Note this does not replace a proper fix, because it is opt-in and cannot be relied upon (see #11862 discussion).
Non-goals / constraints
- Stays opt-in. Our adoption pitch is “usually a one-line change” (
runs-on: macos-latest→runs-on: tuist-macos), and a mandatory${{ github.run_id }}token is exactly the verbosity that pitch avoids. RunsOn keeps theirs opt-in and files it under troubleshooting for the same reason. - Not a security boundary. Because it is opt-in, it cannot carry the fork/cache-capability guarantee.
- Warm-pool efficiency drops for opt-in users: a run-scoped runner cannot be reused by another run, so a mispredicted boot is wasted rather than absorbed.
Open questions
- Token syntax: suffix (
tuist-macos-<token>) vs a RunsOn-style DSL (tuist=<token>/runner=macos). Suffix keeps the current scheme; a DSL is more extensible but a bigger change. - Do we scope by run, or all the way to the job (adding
strategy.job-index)? Run-scoping is enough for trust; job-scoping is needed for full determinism under matrix. - Interaction with the warm pool: do we pre-provision for a scoped label at all, or only boot on demand?
- Whether
requested_dispatch_label(stored onrunner_jobs) should keep the token for debuggability.
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
No GitHub comments yet.