Hardware CI v2 (discovery-driven)¶
Deprecated since version v3: The hw-matrix family is deprecated. New consumers should use the
hw-request family — see Onboarding a Consumer Repo. This page is
retained for repos that have not migrated yet.
The hw-matrix.yml reusable workflow ships in two versions that
coexist:
v1 (
hw-matrix.yml@v1) — manifest-first. Each consumer repo commits a static.github/hw-nodes.json+ atest/hw/env/<place>.yamlper place. The coordinator is queried only to filter dead places out of the manifest. See Hardware CI (v1, manifest-first) for details. v1 stays supported indefinitely.v2 (
hw-matrix.yml@v2) — discovery-driven. The coordinator is the source of truth for live hardware viaplace tags; the consumer repo declares which hardware its tests support via@pytest.mark.iio_hardwaremarkers. No static manifest, no per-place env yaml on the consumer side. This page documents v2.
When to use which¶
Use v2 when your tests are already marked with
@pytest.mark.iio_hardware(the de-facto convention inpyadi-iioand adjacent projects), and the lab admin has tagged every place with the v2 schema.Use v1 otherwise — including the transition window while a given coordinator’s places are being tagged.
The two versions can coexist on the same coordinator with no conflict.
Architecture¶
┌─ labgrid coordinator ─────────────────────────────────────┐
│ Each live place exposes tags: │
│ carrier: zcu102 │
│ daughter-board: ad9081 │
│ boot-strategy: BootFPGASoC │
│ hdl-config: m8_l4 (optional narrowing) │
└─────────────────────┬─────────────────────────────────────┘
│ GET /api/places (REST or labgrid-client fallback)
▼
┌─ hw-matrix.yml@v2 ────────────────────────────────────────┐
│ 1. discover: query coordinator → live places │
│ 2. validate: each place conforms to tag schema │
│ 3. collect: pytest --collect-only -m iio_hardware in │
│ the caller repo │
│ 4. intersect: pair tests with places by daughter-board │
│ 5. render: per matrix entry, write env.yaml from │
│ place tags + boot-strategy template │
│ 6. emit: [(place, daughter, marker_filter)] matrix │
└─────────────────────┬─────────────────────────────────────┘
│
▼
┌─ matrix shard (one per (place, daughter-board)) ──────────┐
│ pytest -m "iio_hardware and <daughter>" --lg-config … │
│ → boot place, run tests, upload JUnit + logs │
└───────────────────────────────────────────────────────────┘
Place-tag schema (lab-admin contract)¶
Every place a v2 workflow may dispatch onto must carry these tags.
Set them via labgrid-client -p <place> set-tags k=v k2=v2 …:
Tag |
Required |
Example values |
Purpose |
|---|---|---|---|
|
yes |
|
Matched against |
|
yes |
|
Primary key. Matched against
|
|
yes |
|
Class name of the labgrid strategy. Picks which env-yaml
template gets rendered. Must be one of the names registered
under |
|
no |
|
Optional further narrowing. Available to tests as an extra field on the matrix entry. |
|
no |
|
Operational only — not used for matrix building. |
The schema validator (adi_lg_plugins.hw_ci.schema.validate_place)
rejects places that:
are missing any required tag, or
set
boot-strategyto a class name not in the live strategy registry (adi_lg_plugins.hw_ci.KNOWN_STRATEGIES).
Skipped places are surfaced as a warning annotation in the
discover job — they don’t crash the workflow.
Test-marker schema (project-side contract)¶
The adi_lg_plugins.pytest_plugin module registers two markers via
the pytest11 entry point. Any project that pip installs
adi-labgrid-plugins gets them auto-registered (no per-project
conftest.py plumbing needed):
import pytest
import adi
@pytest.mark.iio_hardware(["ad9081", "ad9081_tdd"])
def test_rx_buffer(iio_uri):
"""Runs on any place tagged daughter-board=ad9081
(or ad9081_tdd)."""
rx = adi.ad9081(uri=iio_uri)
assert rx.rx().size > 0
@pytest.mark.iio_hardware(["ad9081"])
@pytest.mark.iio_carrier(["zcu102"])
def test_zcu102_only(iio_uri):
"""Only runs on AD9081 attached to a ZCU102 carrier
(not VCU118 or any other carrier with ad9081)."""
…
Single-string shorthand is accepted:
@pytest.mark.iio_hardware("ad9081") is equivalent to
@pytest.mark.iio_hardware(["ad9081"]).
Important
The marker argument must be a string literal (or a literal
list/tuple of strings). The discover step harvests markers by
parsing the AST of every test_*.py file — it does not
import the modules. Computed arguments (variables, f-strings,
list comprehensions) cannot be statically harvested and the
affected test will silently be left out of the matrix. AST harvest
is what lets the coordinator-adjacent runner stay free of the
per-DUT toolchain (libiio, adi, etc.) — those only need to be
present on the per-place hw-<place> runners.
Consumer setup¶
Three things on the consumer side:
Install
adi-labgrid-pluginsin the test venv (the workflow’svenv_install_cmdalready needs to do this — the marker plugin AND the discover CLI both live in this package):adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins.git@v2
Mark tests:
@pytest.mark.iio_hardware(["ad9081"]) def test_x(iio_uri): ...
Add a thin caller workflow:
name: Hardware Tests (GHA) on: workflow_dispatch: pull_request: types: [labeled, opened, synchronize, reopened] schedule: - cron: "0 8 * * *" jobs: hw: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix.yml@v2 with: venv_install_cmd: | uv pip install --quiet --python "$VENV_DIR/bin/python" \ -r requirements_dev.txt uv pip install --quiet --python "$VENV_DIR/bin/python" -e . uv pip install --quiet --python "$VENV_DIR/bin/python" \ "adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins.git@v2" pytest_cmd_template: >- "$VENV_DIR/bin/pytest" -v -m "$MARKER_FILTER" --lg-config "$LG_ENV" --junitxml="$JUNIT" secrets: inherit
That is the entire consumer surface. No .github/hw-nodes.json,
no test/hw/env/*.yaml, no per-place wiring.
Self-hosted runner contract (lab-admin)¶
The reusable workflow expects two classes of self-hosted runner to be
registered against every consumer repo that calls into @v2:
One coordinator-adjacent runner labeled
[self-hosted, hw-coordinator]. Thediscoverjob runs here. It only needs to be able to reach the coordinator’s REST API and runpytest --collect-onlyagainst the caller repo — no DUT access is required.One runner per coordinator place, labeled
[self-hosted, hw-<place>]. Thehwmatrix shard for a given place pins tohw-<place>— e.g. shard for placemini2lands on the runner labeledhw-mini2. This runner must be able to power-cycle and reach the DUT(s) behind that place.
This convention deliberately does not require any extra place
tag: the place name itself drives the routing. To bring up a new
place foo, register a self-hosted runner with labels
[self-hosted, hw-foo] and tag the place per the schema above —
no workflow or repo change is required.
Override the coordinator-runner label with the
coordinator_runner_label input if your lab uses a different
naming scheme. The per-shard hw-<place> mapping is fixed by the
workflow (it derives from matrix.entry.place).
Migration from v1¶
If you’re currently on v1:
Tag your places (lab-admin task):
labgrid-client -x $LG_COORDINATOR -p mini2 set-tags \ carrier=zcu102 daughter-board=ad9081 boot-strategy=BootFPGASoC
In the consumer repo:
rm .github/hw-nodes.json rm test/hw/env/*.yaml rmdir test/hw/env
Add
@pytest.mark.iio_hardware([…])to any tests that don’t already have it. (pyadi-iio’s legacytest/test_*.pyalready has these markers — they keep working without change.)Flip your workflow to
@v2and update the inputs as shown above.
Empty-intersection behaviour¶
If the project marks tests for hardware that’s offline right now, the
matrix is empty. The discover job emits a clear warning
annotation in the GHA UI:
hw-ci discovery produced 0 matrix entries — no marked tests
overlap with live coordinator hardware right now
The job itself succeeds (not failure), so the workflow doesn’t block PR merges purely because the lab is down. Nightly cron runs will re-trigger as boards come back up.
CLI reference¶
The adi-lg-hw-ci command is what the reusable workflow shells out
to; it’s also runnable locally for debugging:
# Print the matrix that would be emitted (no GHA $GITHUB_OUTPUT)
adi-lg-hw-ci discover \
--coord 10.0.0.41:20408 \
--test-root . \
--marker iio_hardware
# Render the env yaml for one place to a file
adi-lg-hw-ci render-env \
--coord 10.0.0.41:20408 \
--place mini2 \
--out /tmp/env-mini2.yaml
# Sanity-check: which strategies have render templates?
adi-lg-hw-ci list-strategies