Runner Labels and Targeting
Runners can carry free-form labels — short tags such as linux, gpu,
prod, staging, region-eu. When you launch a load test, fuzzing run,
chaos experiment, or test plan, Mockarty can route the task to a specific
subset of runners by checking their labels.
There are two ways to express the requirement:
| Mode | When to use | What you write |
|---|---|---|
| Simple (chips) | Every runner must have all of these labels. The common case. | linux, gpu, prod (chip per label, implicit AND) |
| Advanced (DSL) | You need OR, NOT, regex, or grouping. | `linux & (gpu |
In the UI the picker has a Simple / Advanced toggle in the top-right
of the chip row. Switching from Simple to Advanced pre-fills the
expression with the current chips joined by &. Switching back to
Simple is only allowed when the current expression is a pure AND of
literal atoms.
Setting labels on a runner
Open Admin → Integrations, click Edit labels next to the
runner, and add chips. Labels must be 1–64 characters from
a-z, 0-9, ., -, _. Uppercase characters are normalised to
lowercase.
You can also pass labels when starting an ephemeral runner via the
RUNNER_LABELS environment variable (comma-separated, e.g.
RUNNER_LABELS=linux,gpu,ci).
Simple mode (chips)
Add one or more chips. A runner is eligible only if its label set is a
superset of your chip list. For example:
| Chips | Eligible runners |
|---|---|
linux |
every runner that carries the linux label |
linux, gpu |
only runners that carry both linux and gpu |
Order does not matter. Adding more chips never broadens the match —
each chip narrows it.
Advanced mode (label DSL)
Toggle Advanced and write an expression. The grammar:
expr := orExpr
orExpr := andExpr ('|' andExpr)*
andExpr := notExpr ('&' notExpr)*
notExpr := '!' notExpr | atom
atom := literal | regex | '(' expr ')'
literal := [a-z0-9._-]{1,64}
regex := '/' pattern '/' flags?
flags := 'i'
&— AND (both sides must match)|— OR (at least one side matches)!— NOT (the side must not match)(...)— grouping, overrides precedence/.../— regex against label set, matches if any label matches
the pattern. Only thei(case-insensitive) flag is allowed.
Precedence (highest first): !, &, |. So
linux & gpu | tpu parses as (linux & gpu) | tpu. Use parentheses
when in doubt.
Examples
linux & gpu same as Simple [linux, gpu]
linux & (gpu | tpu) linux + at least one accelerator
linux & !staging Linux, but not the staging fleet
/^perf-.*/i & ci any "perf-…" runner that's also "ci"
(prod | staging) & region-eu EU-only, prod or staging
!arm64 any non-ARM runner
Limits
The parser enforces hard caps so a malformed expression cannot make
the dispatcher do unbounded work:
| Limit | Value |
|---|---|
| Maximum expression length | 1024 bytes |
| Maximum nesting depth | 16 |
| Maximum literal + regex atoms | 64 |
| Maximum regex atoms | 8 |
| Maximum regex pattern length | 256 bytes |
| Allowed regex flags | i only |
If you exceed a limit the validate badge under the input shows the
exact byte offset and the rule that was violated.
Live validation
As you type, Mockarty calls POST /api/v1/runner-labels/validate with
your expression and shows one of:
matches N online runners— green, your expression parses and N
online runners satisfy it. Up to 5 example runner names are previewed
in the matched-runners list under the picker.matches 0 online runners— amber, parses fine but nothing
matches right now. The job will queue and wait for a matching
runner to come online.Invalid expression at byte N: <reason>— red. Fix the syntax
before you submit; the dispatcher rejects a malformed expression.
Validation is debounced 300 ms so quick edits do not spam the server.
Where labels apply
Targeting works the same way everywhere a job lands on a runner:
| Surface | Field name on the wire |
|---|---|
POST /api/v1/perf/run |
requiredRunnerLabels[] or runnerLabelExpr |
POST /api/v1/fuzzing/run |
requiredRunnerLabels[] or runnerLabelExpr |
| Test Plan item | runnerLabels[] (per item) |
| API tester functional run | requiredRunnerLabels[] or runnerLabelExpr |
| Test Case / UI test run | inherited from the run’s TCM Configuration (see below) |
requiredRunnerLabels and runnerLabelExpr are mutually exclusive in
the UI — the picker emits one or the other depending on its mode. If
both reach the dispatcher they are AND-combined (so the expression
narrows the chip set, never widens it).
Routing Test Case runs (and UI tests) via a Configuration
A Test Case run does not carry a label picker of its own — instead it
inherits the runner-selector from the TCM Configuration it is launched
against. This keeps the selector reusable: “the EU smoke configuration
always runs on the region-eu fleet” is declared once on the
configuration and every run that uses it is routed the same way. Every
external-runner step the run dispatches — UI test (browser runner),
performance, API-test, fuzzing — inherits the same selector.
A configuration declares its selector with two optional fields:
| Field | Type | Meaning |
|---|---|---|
runnerLabels |
array of strings | Simple-mode chips — a runner must carry all of them (superset, implicit AND). |
runnerLabelExpr |
string | Advanced-mode label DSL (same grammar as above). AND-combined with runnerLabels. |
Set them on the configuration through the configurations API, e.g.:
# Create a configuration pinned to the EU GPU fleet
curl -X POST "$MOCKARTY/api/v1/namespaces/$NS/tcm/configurations" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"name": "EU smoke",
"isDefault": true,
"params": { "runnerLabels": ["region-eu", "gpu"] }
}'
When you start a case run, pick the configuration with the optional
configurationId field:
curl -X POST "$MOCKARTY/api/v1/namespaces/$NS/test-cases/$CASE_ID/run" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{ "mode": "semi_automatic", "configurationId": "'$CONFIG_ID'" }'
- Explicit
configurationId— the run uses that configuration’s
selector. An unknown id (or one from another namespace) is rejected
with400/404so a typo never silently runs unscoped. - No
configurationId— the run falls back to the case’s
default-linked configuration (the one marked default). If the case
has no default configuration, the run is unscoped (matches by namespace- capability only, exactly as before).
If the configuration declares no selector, the run is unscoped. A run
whose selector matches no online runner queues and waits for a matching
runner to come online (UI tests additionally require a runner that
advertises the ui-test capability — see UI Test Recording).
Simple vs. Advanced — which should I use?
Pick Simple when:
- the rule is “every runner must have these tags”;
- the chip set is short (1–4 tags);
- you do not need OR / NOT / regex.
Pick Advanced when:
- runners are split into pools you want to address by name pattern
(/^perf-.*/i); - you want OR (“any runner with
gpuORtpu”); - you want NOT (“everything except
staging”); - you have nested rules.
Simple mode is faster to type and read at a glance; Advanced is
strictly more expressive but harder to skim. For a CI runbook,
Advanced expressions live well in a version-controlled config; for
ad-hoc launches Simple is usually enough.
Best practices
- Treat labels like Kubernetes node labels — describe what the
runner is, not what the job needs (gpu✓,ml-training-job
✗). - Keep label sets small. The chip picker autocompletes from existing
labels so a sprawling vocabulary becomes hard to navigate. - Use a stable naming convention:
region-eu,region-us,os-linux,
arch-amd64. The dot/dash convention scans well in expressions. - Reserve regex matches for the case where a literal set genuinely
cannot describe the cohort. Regex evaluation is the slowest path. - For pinned hardware (e.g. a specific GPU model), prefer a dedicated
literal label (gpu-a100) over a regex (/^gpu-a100.*/).