Docs Runner Labels and Targeting

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 the i (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
    with 400 / 404 so 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 gpu OR tpu”);
  • 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.*/).