This file is for AI coding agents (and humans) wiring a new consumer repo onto the
labgrid-plugins hardware-CI flow. It is an executable recipe: follow the steps, run the
verify commands, then open the PR. The human reference with full prose is
docs/source/user-guide/onboarding-a-consumer-repo.rst; deep per-topic docs are linked
from there. Copy-paste starting files live in adi_lg_plugins/hw_ci/onboarding_templates/.
Repo-level guidance for working inside this package is in
CLAUDE.md. This file is specifically about onboarding other repos as hardware-CI consumers.
A consumer repo's CI calls a reusable workflow here. A preflight job discovers which
of the consumer's wanted boards are live on the lab coordinator, then fans out one CI
leg per board to a self-hosted runner co-located with that board. The board is
reserved, provisioned, exercised, and released automatically — the consumer never defines
labgrid drivers/strategies or touches a board directly. Boards are boot-verified (iiod
reachable) before tests run, and boot failures are reported distinctly (exit 12 plus an
::error title=boot-failure:: annotation) so they never masquerade as test failures.
| If the consumer… | Mode | Reusable workflow | Discovery |
|---|---|---|---|
| runs Python/pytest against a booted Linux board over libIIO (a URI) | uri | hw-request.yml |
@pytest.mark.iio_hardware(["<part>"]) markers |
| builds bare-metal firmware, JTAG-flashes it, validates over serial | flash | noos-hw-request.yml |
a tools/hw_ci/projects.yaml manifest |
runs MATLAB runHWTests against a URI |
matlab | matlab-hw-request.yml |
a board_map.yaml |
drives boot itself via labgrid (pytest plugin + LG_ENV, e.g. per-test DTBs) |
uri workflow, request-mode: reserve |
hw-request.yml with request-mode: "reserve" |
@pytest.mark.iio_hardware(["<part>"]) markers |
Deprecation notice:
hw-matrix.ymlandhw-matrix-v2.ymlare deprecated. New consumers must use the hw-request family (hw-request.yml,noos-hw-request.yml,matlab-hw-request.yml) pinned at@v3.5(current release). Removal of the deprecated workflows is tracked by the HW-CI convergence effort.
Reference consumers: pyadi-iio (uri), no-os (flash), TransceiverToolbox (matlab). Matlab
now has a drop-in template at adi_lg_plugins/hw_ci/onboarding_templates/matlab-hw-request.yml and the
reusable matlab-hw-request.yml workflow — see Step 2 below. The rest of this file covers
all three modes.
Copy the matching template(s) from adi_lg_plugins/hw_ci/onboarding_templates/ and replace the
<PLACEHOLDERS>:
uri mode
.github/workflows/hw-request.yml←hw-request-uri.yml(settest-root,install-cmd).test/hw/conftest.py←conftest-iio-uri.py(theiio_urifixture).- Mark the hardware tests:
@pytest.mark.iio_hardware(["<part>"])— string literals only (the preflight AST-parses them; variables/f-strings are silently invisible). - Optional carrier narrowing:
@pytest.mark.iio_carrier(["<carrier>"]).
flash mode
.github/workflows/hw-request.yml←noos-hw-request-flash.yml.tools/hw_ci/projects.yaml←projects.yaml(one entry per buildable project).- Each
projects/<noos_project>/mustmakean.elf(+ the workflow extracts the bitstream + ps7_init from the Kuiper.xsa).
matlab mode
.github/workflows/hw-matlab.yml←matlab-hw-request.yml(setcoordinator,runner-label,preflight-runner-label,matlab-bin).test/hw_ci/board_map.yaml— maps(daughter-board, carrier, hdl-config)to the MATLAB board name passed torunHWTests. Most-specific entry wins. Example:boards: - {carrier: zcu102, daughter-board: adrv9002, matlab_board: zynqmp-zcu102-rev10-adrv9002-vcmos} - {daughter-board: pluto, matlab_board: pluto}
runHWTests.mreads$IIO_URI(exported byadi-lg request) and emits<matlab_board>_HWTestResults.xml— no test-side changes needed. The leg runner must have MATLAB installed (+ a reachable license).- One extra repo var beyond the Step-3 three:
MATLAB_BIN(path to thematlabbinary on the runner, e.g./opt/MATLAB/R2025b/bin/matlab).
Drop adi_lg_plugins/hw_ci/onboarding_templates/AGENTS-consumer-stub.md into the consumer repo as
its own AGENTS.md and fill in the wiring.
In the consumer repo: Settings → Secrets and variables → Actions → Variables:
| Variable | Value | Notes |
|---|---|---|
LG_COORDINATOR |
<host>:20408 |
the gRPC coordinator port — NOT the REST :8000. The workflow derives the REST API from it. |
HW_REQUEST_RUNNER |
e.g. hw-lab |
fallback runner label for the per-board legs |
HW_PREFLIGHT_RUNNER |
e.g. hw-coordinator |
runner label that can reach the coordinator |
If unset, matrix jobs receive empty values and fail.
Optional — Prism result reporting (hw-request.yml / matlab-hw-request.yml): two
more repo variables — PRISM_UPLOAD_ENABLED (true enables the prism-upload input)
and PRISM_URL (Prism base URL) — plus three Actions secrets passed explicitly in
the caller's secrets: block: PRISM_API_TOKEN, PRISM_EMAIL, PRISM_PASSWORD
(cross-org secrets: inherit does NOT work). See "Uploading results to Prism" in
docs/source/user-guide/hw-request.rst.
These live coordinator-side and lab-side; an agent cannot create them but MUST verify they exist (Step 5 will fail clearly if they don't):
-
Catalog entry for each
partincoordinator/api/board_catalog.yaml(schema:coordinator/api/app/catalog.py→BoardEntry/FlashConfig; template:adi_lg_plugins/hw_ci/onboarding_templates/board-catalog-entry.yaml). uri needsimage; flash needs aflash:block. After any catalog edit the coordinator host must be redeployed (it does not auto-update). -
A live place tagged
daughter-board=<part> carrier=<carrier> boot-strategy=<Strategy>(+ optionalrunner=<label>).Hand the lab admin this (fill the placeholders) so the catalog + place round-trip is one message:
Please add to coordinator/api/board_catalog.yaml: a <part> entry (uri: image; flash: flash block), and create a live place tagged: daughter-board=<part> carrier=<carrier> boot-strategy=<Strategy> (+ runner=<label> if board-pinned). Redeploy the coordinator after merging. -
Runner scope: the lab runners must be registered on the consumer repo's (or its org's) GitHub scope, or legs queue forever. See
.github/scripts/register-hw-runners.sh --scopes. -
flash only: the leg runner needs Vivado/Vitis installed (the workflow installs the
[kuiper]/pytsk3 dep and sources the.xsafrom the Kuiper image itself) and ~10 GB free disk for the Kuiper image.
Install the package (pip install -e ".[dev]" here, or pip install labgrid-plugins)
and run the discovery preflight against the live coordinator — this proves the markers/
manifest + catalog + places line up:
export LG_COORDINATOR=<host>:20408
# uri mode: harvest markers under test-root, intersect with live boards
adi-lg-hw-ci request-matrix --test-root <test/hw> --coord "$LG_COORDINATOR"
# flash mode: intersect the manifest with live flash-capable boards
adi-lg-hw-ci noos-matrix --manifest tools/hw_ci/projects.yaml --coord "$LG_COORDINATOR"
# matlab mode
adi-lg-hw-ci matlab-matrix --board-map test/hw_ci/board_map.yaml --coord "$LG_COORDINATOR"Success = the printed matrix.include has one leg per board you expect, each with a
non-empty runner. A wanted board with no live place is emitted as a ::warning:: skip
(that means the catalog/place is missing — Step 4).
If a board prints
Unknown release version, the coordinator catalog is stale — ask the lab admin to redeploy the coordinator after the catalog merge.
Other quick checks:
adi-lg-hw-ci list-strategies # the board's boot-strategy must appear here
# flash only — prove the .xsa is extractable for a board:
adi-lg-hw-ci fetch-xsa --release <2023_R2_P1> --board <canonical-board> --carrier <carrier>Then trigger the workflow (workflow_dispatch, or add the hw-request PR label) and
confirm both the preflight and the per-board legs go green.
LG_COORDINATORmust be the gRPC:20408, not REST:8000.@pytest.mark.iio_hardware(...)args must be string literals (AST-parsed; computed lists are invisible to discovery).- The three repo vars must exist before the first run.
- Runner scope: a runner registered only for
repo:a/bcannot serverepo:a/c— each consumer repo (or the org) needs its own registration. - Place tags: missing
daughter-board/carrier/boot-strategy, or an unknownboot-strategy, silently drops the place from matching. - flash
kuiper_xsa_dir: some boards' Kuiper boot folders are named for a family (e.g. adrv9371 lives in…-adrv937x), so the*<carrier>*<board>*search misses — the catalogflash.kuiper_xsa_diroverride pins the folder. - flash runner: needs Vivado + the
[kuiper]/pytsk3 install + ~10 GB disk for the Kuiper image. - Random MACs on stock images: stock Kuiper boot files randomize the DUT MAC every
boot → fresh DHCP lease/IP per boot. The interactive TFTP path (
BootFPGASoCTFTP) auto-derives a stable per-place MAC and runssetenv ethaddrbeforedhcp; override with the place tagethaddr=<mac>, opt out withethaddr=stock.sd-autobootboards boot U-Boot's own env — setethaddrin the SD'suEnv.txtinstead. - After editing the coordinator catalog, redeploy the coordinator host — it does not auto-update.
- Manifest schema:
adi_lg_plugins/hw_ci/noos_manifest.py(NoOSProject). - Place-tag schema:
adi_lg_plugins/hw_ci/schema.py(Place, required/optional tags). - Catalog schema:
coordinator/api/app/catalog.py(BoardEntry,FlashConfig). - Reusable workflow inputs:
docs/source/user-guide/github-actions.rst. - CLI:
docs/source/user-guide/cli.rst(adi-lg-hw-ci,adi-lg request). - Pinning: consumer
uses:lines must reference@v3.5(current release), e.g.uses: tfcollins/labgrid-plugins/.github/workflows/hw-request.yml@v3.5. Bump the pin when a new release tags. Never pin to@mainin production workflows.