GitHub Actions (Reusable Workflows)¶
This repo hosts reusable GitHub Actions workflows that consumer repos
(pyadi-iio, pyadi-dt, vrt49, no-os) call via
uses: tfcollins/labgrid-plugins/.github/workflows/<name>.yml@<ref>.
The repo must remain public so cross-org callers can reference it
without an enterprise allowlist. Each workflow runs a preflight job that
discovers live boards from the coordinator and fans out one CI leg per
matching board; no test will queue against hardware that is currently
unreachable.
Note
Two composite actions (setup-uv-venv, acquire-place) and a
runner-registration helper (register-hw-runners.sh) are documented
in the Composite actions and Registering self-hosted runners sections
below.
Choosing a workflow¶
Warning
hw-matrix.yml and hw-matrix-v2.yml are 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.
Workflow |
What it runs |
Discovery mechanism |
Use when |
|---|---|---|---|
|
pytest against places from a committed manifest |
|
Do not adopt for new consumers; use |
|
pytest against discovered places; renders env.yaml per shard |
|
Do not adopt for new consumers; use |
|
|
|
Low-config: consumers name a part, labgrid boots and hands over a URI (pyadi-iio pattern). |
|
MATLAB |
|
MATLAB toolbox hardware tests (TransceiverToolbox pattern). |
|
Build no-os firmware → JTAG-flash → validate on serial (flash mode) |
|
no-os bare-metal firmware on real boards. |
hw-matrix.yml — manifest-first matrix¶
Probes the coordinator for available labgrid places listed in a committed
hw-nodes.json manifest, then fans out a per-place matrix with optional
hw-direct and/or hw-coord legs. Includes a dynamic_mode option
that switches to API-driven discovery without the static manifest. See
Hardware CI (v1, manifest-first) for the full consumer contract, manifest schema, and
migration notes.
hw-matrix.yml example¶
# .github/workflows/hw.yml (consumer repo)
name: HW
on: [pull_request]
jobs:
hw:
uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix.yml@main
with:
venv_install_cmd: >-
uv pip install --quiet --python "$VENV_DIR/bin/python" -e ".[dev]"
pytest_cmd_template: >-
"$VENV_DIR/bin/pytest" $TESTS --junitxml="$JUNIT" -v
secrets:
PYADI_BUILD_TOKEN: ${{ secrets.PYADI_BUILD_TOKEN }}
hw-matrix.yml inputs¶
Input |
Required |
Default |
Description |
|---|---|---|---|
|
No |
|
Coordinator |
|
No |
|
Repo-relative path to the hw-nodes manifest. |
|
Yes |
— |
Shell command run with |
|
No |
|
Persistent venv root on each runner host. |
|
No |
|
Shell command run in each matrix leg before pytest (cmake builds, cross-compilation, fixture pre-staging). |
|
Yes |
— |
Shell command run after |
|
No |
|
Newline-separated globs uploaded on every leg. |
|
No |
|
Comma-separated subset of legs to consider. Per-place legs in the manifest override this. |
|
No |
|
Per-leg timeout (covers acquire-wait + pytest + teardown). |
|
No |
|
How long to wait for a busy place before failing the leg. |
|
No |
|
Path to a |
|
No |
|
Post JUnit + artifact bundle to Prism after each leg. |
|
No |
|
Prism base URL. Defaults to |
|
No |
|
Prism project slug. Required when |
|
No |
|
Consumer-supplied shell command to upload results to Prism. The
step exports |
|
No |
|
Opt in to API-driven discovery: places are fetched from the
coordinator and filtered against |
|
No |
|
Repo-relative path to a YAML with a |
|
No |
|
Base URL of the coordinator FastAPI bridge (e.g.
|
|
No |
|
Fallback runner label for dynamic-mode legs when the place has no
|
|
No |
|
Place-tag key matched against |
|
No |
|
|
hw-matrix-v2.yml — discovery-driven matrix¶
Unlike v1, v2 discovers live hardware from the coordinator’s /api/places
endpoint and intersects it with the caller’s @pytest.mark.iio_hardware
and @pytest.mark.iio_carrier markers. The consumer repo ships no
hw-nodes.json manifest and no per-place env yaml; the env yaml is
rendered per-shard from the place’s boot-strategy tag. See
Hardware CI v2 (discovery-driven) for the full consumer contract.
hw-matrix-v2.yml example¶
# .github/workflows/hw.yml (consumer repo)
name: HW
on: [pull_request]
jobs:
hw:
uses: tfcollins/labgrid-plugins/.github/workflows/hw-matrix-v2.yml@main
with:
venv_install_cmd: >-
uv pip install --quiet --python "$VENV_DIR/bin/python" -e ".[dev]"
"adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins@main"
pytest_cmd_template: >-
"$VENV_DIR/bin/pytest" -m "$MARKER_FILTER" --junitxml="$JUNIT"
secrets:
PYADI_BUILD_TOKEN: ${{ secrets.PYADI_BUILD_TOKEN }}
hw-matrix-v2.yml inputs¶
Input |
Required |
Default |
Description |
|---|---|---|---|
|
No |
|
Coordinator |
|
No |
|
Top-level pytest marker used to harvest test-to-hardware affinity. Non-IIO projects can swap this. |
|
No |
|
Path (relative to repo root) for |
|
Yes |
— |
Shell command run with |
|
No |
|
Persistent venv root on each runner host. |
|
No |
|
Optional command run inside each shard before pytest (cmake builds, fixture pre-staging). |
|
Yes |
— |
Shell command run in each shard. The workflow exports
|
|
No |
|
Newline-separated globs uploaded on every leg. |
|
No |
|
Per-leg timeout. |
|
No |
|
Maximum wait time for a busy place. |
|
No |
|
Post results to Prism after each leg. |
|
No |
|
Prism base URL. Defaults to |
|
No |
|
Prism project slug. Required when |
|
No |
|
Extra label the coordinator-adjacent runner must carry (alongside
|
hw-request.yml — low-config by-part¶
Runs a consumer repo’s hardware tests by part: adi-lg-hw-ci
request-matrix intersects the caller’s @pytest.mark.iio_hardware
markers with the coordinator’s /api/match endpoint, then fans out one
independent job per matching part. Each leg calls adi-lg request --part
<p> which boots a free board and runs pytest with IIO_URI injected.
No place names, env yaml, or board maps are needed in the consumer repo.
See Hardware CI by part (hw-request) for the full consumer contract.
hw-request.yml example¶
# .github/workflows/hw.yml (consumer repo, e.g. pyadi-iio)
name: HW
on: [pull_request]
jobs:
hw:
uses: tfcollins/labgrid-plugins/.github/workflows/hw-request.yml@v3.5 # bump when a new release tags
with:
coordinator: ${{ vars.LG_COORDINATOR }}
test-root: "test"
install-cmd: >-
uv pip install --quiet --python "$VENV_DIR/bin/python" -e "."
"adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins@v3.5"
hw-request.yml inputs¶
Input |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
— |
Coordinator |
|
No |
|
Path to the test directory to harvest markers from and run. |
|
No |
|
Top-level pytest marker gating hardware tests. |
|
No |
|
Seconds each leg queues for a free matching board ( |
|
No |
|
Fallback self-hosted runner label for per-board legs. Each leg
prefers the runner its board is wired to (the place’s |
|
No |
|
Runner label for the discovery preflight (must reach the coordinator REST API). |
|
No |
|
Extra args appended to the per-leg pytest command. |
|
No |
|
Absolute path for the persistent uv venv on the runner. |
|
No |
|
Shell command (run with |
|
No |
|
|
|
No |
|
Post each leg’s JUnit to Prism after the test run. The upload step
runs with |
|
No |
|
Prism base URL. Defaults to the caller’s |
|
No |
|
Prism project slug. Required when |
|
No |
|
Consumer-supplied shell command that uploads results to Prism
(vendored-uploader escape hatch, e.g. for a private Prism repo).
Used instead of the built-in uploader when non-empty. The step
exports |
hw-request.yml secrets¶
All secrets are optional; the Prism ones are only consulted when
prism-upload is true. Cross-org callers must pass them
explicitly in the caller’s secrets: block — secrets: inherit
does not cross org boundaries.
Secret |
Required |
Description |
|---|---|---|
|
No |
PAT exposed as a |
|
No |
API token used by the Prism upload step. |
|
No |
Login email for the Prism upload step (login-auth uploaders). |
|
No |
Login password for the Prism upload step (login-auth uploaders). |
matlab-hw-request.yml — MATLAB by-part¶
Boots a matching board via labgrid, runs the MATLAB toolbox’s
runHWTests(<board>) against the booted board’s libIIO URI, collects the
JUnit, and releases the board — one independent job per board. The
preflight runs adi-lg-hw-ci matlab-matrix, intersecting the consumer’s
board_map.yaml with the coordinator’s live places. Each leg uses the
same adi-lg request core as the uri/flash flows. The leg runner must
have MATLAB installed (+ a reachable license) and the libIIO libs.
matlab-hw-request.yml example¶
# .github/workflows/hw-matlab.yml (consumer repo, e.g. TransceiverToolbox)
name: HW MATLAB
on: [pull_request]
jobs:
hw-matlab:
uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.5 # bump when a new release tags
with:
coordinator: ${{ vars.LG_COORDINATOR }}
board-map: "test/hw_ci/board_map.yaml"
matlab-bin: ${{ vars.MATLAB_BIN }}
matlab-hw-request.yml inputs¶
Input |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
— |
gRPC coordinator |
|
No |
|
Path (in the consumer checkout) to the MATLAB |
|
No |
|
Fallback self-hosted runner label for the per-board legs. |
|
No |
|
Runner label for the discovery preflight (reaches the coordinator). |
|
No |
|
Path to the MATLAB binary on the leg runner. |
|
No |
|
Seconds each leg queues for a free matching board ( |
|
No |
|
Absolute path for the persistent uv venv on the runner. |
|
No |
|
Command (with |
|
No |
|
Post each leg’s JUnit to Prism after the test run. The upload step
runs with |
|
No |
|
Prism base URL. Defaults to the caller’s |
|
No |
|
Prism project slug. Required when |
|
No |
|
Consumer-supplied shell command that uploads results to Prism
(vendored-uploader escape hatch, e.g. for a private Prism repo).
Used instead of the built-in uploader when non-empty. The step
exports |
matlab-hw-request.yml secrets¶
Identical to the hw-request.yml secrets above: optional
PRISM_API_TOKEN / PRISM_EMAIL / PRISM_PASSWORD, only consulted
when prism-upload is true, and passed explicitly in the caller’s
secrets: block (cross-org secrets: inherit does not work).
noos-hw-request.yml — no-os firmware flash¶
Builds a no-os reference project, JTAG-flashes it onto a matching physical
board via labgrid, and validates it on-target with a serial banner assertion —
one independent job per project. The preflight runs adi-lg-hw-ci
noos-matrix, intersecting the consumer’s projects.yaml manifest with
the coordinator’s live FLASH-capable boards (GET /api/match?mode=flash).
See Hardware-CI Runner Setup (no-os flash mode) for runner requirements, Vivado setup,
and the manifest reference.
noos-hw-request.yml example¶
# .github/workflows/noos-hw.yml (consumer repo, e.g. no-os)
name: no-os HW
on: [pull_request]
jobs:
noos-hw:
uses: tfcollins/labgrid-plugins/.github/workflows/noos-hw-request.yml@v3.5 # bump when a new release tags
with:
coordinator: ${{ vars.LG_COORDINATOR }}
manifest: "tools/hw_ci/projects.yaml"
noos-hw-request.yml inputs¶
Input |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
— |
Coordinator |
|
No |
|
Path (in the consumer checkout) to the no-os hw-ci
|
|
No |
|
Fallback self-hosted runner label for per-project legs. Each leg
prefers the runner co-located with its board’s JTAG + Vivado (the
place’s |
|
No |
|
Runner label for the discovery preflight (must reach the REST API). |
|
No |
|
Shell command to build a project (cwd = consumer checkout).
Receives |
|
No |
|
Built |
|
No |
|
FPGA bitstream path to flash before the |
|
No |
|
|
|
No |
|
Fallback serial banner asserted on-target after flashing. Each matrix leg uses its own manifest-declared banner; this is the global default. |
|
No |
|
Seconds each leg queues for a free matching board ( |
|
No |
|
Absolute path for the persistent uv venv on the runner. |
|
No |
|
Shell command (run with |
Composite actions¶
Both composite actions are used internally by every reusable workflow above and can also be called standalone from any consumer repo.
setup-uv-venv¶
Ensures uv is installed on the runner, creates (or reuses) a persistent
venv at the requested path, runs the caller’s install command, and adds the
venv’s bin/ directory to $GITHUB_PATH for subsequent steps.
- uses: tfcollins/labgrid-plugins/.github/actions/setup-uv-venv@v3.5 # bump when a new release tags
with:
venv_dir: "$HOME/.cache/hw-ci/venv"
install_cmd: >-
uv pip install --quiet --python "$VENV_DIR/bin/python" -e ".[dev]"
Input |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
— |
Absolute path to the persistent venv directory (created if missing). |
|
Yes |
— |
Shell command run with |
|
No |
|
Python interpreter passed to |
Outputs
Output |
Description |
|---|---|
|
Absolute path to the venv’s Python interpreter. |
acquire-place¶
Reserves and acquires a labgrid place via the coordinator, waiting up to
wait_minutes for it to become free. Uses labgrid’s reservation queue
when available, falling back to a jittered polling loop.
Note
acquire-place is only for the deprecated bash / hw-matrix flow. hw-request-family
consumers do not need it — reservation + release are handled inside the reusable workflow
by adi-lg request.
Warning
Composite actions cannot run post-job cleanup. Every caller must
pair this action with a release step guarded by if: always() that
calls labgrid-client … release.
- uses: tfcollins/labgrid-plugins/.github/actions/acquire-place@v3.5 # bump when a new release tags
with:
coordinator: ${{ env.COORDINATOR }}
place: my-zcu102
labgrid_client: "$HOME/.cache/hw-ci/venv/bin/labgrid-client"
wait_minutes: "30"
Input |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
— |
Coordinator URL ( |
|
Yes |
— |
labgrid place name to acquire. |
|
No |
|
Path to the |
|
No |
|
Maximum time to wait for the place to become free, in minutes. |
Outputs
Output |
Description |
|---|---|
|
Reservation token string, if a reservation was created. |
|
ISO-8601 timestamp when the place was successfully acquired. |
Registering self-hosted runners¶
.github/scripts/register-hw-runners.sh installs GitHub Actions
self-hosted runner services on lab hosts via SSH. A single physical host
can serve multiple GitHub scopes simultaneously — for example, both an org
scope and a personal-repo scope — with one runner service per scope sharing
the same lab YAML via the same LG_DIRECT_ENV path.
Prerequisites (on the machine running the script):
ghCLI authenticated againstgithub.comwithadmin:orgfor anyorg:scopes, andrepofor anyrepo:scopes.SSH key-based access as the target user on each lab host.
Local tools:
gh,jq,ssh,scp,mktemp.
Flags¶
--hosts-file <path>(required)TSV file listing the lab hosts to configure (see format below).
--scopes <list>(required)Comma-separated GitHub scopes to register against. Each scope must be prefixed with either
org:(for an organisation) orrepo:(for a specific repository, asowner/repo). Example:org:analogdevicesinc,repo:tfcollins/labgrid-plugins-h/--helpPrint usage and exit.
Hosts-file format¶
A TSV file; lines beginning with # are comments. Five
tab-separated columns:
# alias ssh_target runner_label runner_name_base lg_direct_env_path
bq bq hw-bq bq /home/tcollins/dev/dt-fix/lg_adrv9371_zc706_tftp.yaml
mini2 mini2 hw-mini2 mini2
Columns:
alias — short name used on the command line as an optional filter.
ssh_target — SSH host (name or
user@host) used byssh/scp.runner_label — label attached to every runner on this host (e.g.
hw-bq); used as theruns-onlabel in per-leg jobs.runner_name_base — base for the runner name; the script appends
-<scope-slug>to produce the final name (e.g.bq-org-analogdevicesinc).lg_direct_env_path — (optional) path on the remote host to write as
LG_DIRECT_ENV=<path>in the runner’s.envfile. Omit for coordinator-only runners.
For each host × scope pair the script installs a runner under
~/actions-runner-<scope-slug>/, configures it with
--labels "self-hosted,<runner_label>", and starts a systemd service.
Worked example¶
.github/scripts/register-hw-runners.sh \
--hosts-file ./hosts.tsv \
--scopes org:analogdevicesinc,repo:tfcollins/labgrid-plugins,repo:tfcollins/vrt49 \
bq mini2 # optional: restrict to these two hosts only
After registration the script waits 60 seconds and queries GitHub to confirm
every runner is online.
Cross-reference: Hardware-CI Runner Setup (no-os flash mode) documents the additional
Vivado/xsdb requirements for no-os flash-mode runners.
Runner labels and cross-org scope¶
Each per-board leg in the reusable workflows pins its runner with:
runs-on: [self-hosted, "${{ matrix.runner || inputs.runner-label }}"]
The matrix.runner value comes from the place’s runner tag on the
coordinator — it is the label of the GitHub Actions runner co-located with
that board. When a place carries no runner tag the workflow falls back
to its runner-label input (the hw-request / noos-hw-request /
hw-matrix-v2 workflows), or to dynamic_runner_label_default in
hw-matrix.yml’s dynamic mode.
Cross-org consumers must register the lab runners on their repo or org
scope using register-hw-runners.sh --scopes. If no runner carrying the
required label is online in the consuming repo’s scope, the job will queue
indefinitely rather than fail immediately — use RUNNER_QUERY_TOKEN in
hw-matrix.yml (dynamic_mode) to enable availability filtering that
skips legs with no reachable runner.