Hardware CI for bash / non-Python test drivers (UART + JTAG)¶
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 reusable workflow and the v2 discovery flow both
assume the test driver is pytest: a labgrid fixture in the
consumer’s conftest.py owns the board lifecycle and tests reach the
DUT over the network (a libIIO ip:<addr> URI or SSH).
Some projects don’t fit that mold. Their test driver is a bash / non-Python harness, and it drives the board over the serial console (UART) and JTAG (xsdb) rather than a network URI. This page documents a CLI-driven pattern for those projects: a single CI job launches the board as a distinct step, then a separate shell step runs the external test driver against the booted board.
Note
For pytest-driven projects use Hardware CI v2 (discovery-driven) instead — the labgrid fixture owns boot/teardown and there is nothing to hand off. This page is only for non-Python drivers that talk UART + JTAG.
When to use this¶
Your tests are a shell script (or any non-Python tool) — there is no pytest process for a labgrid fixture to hook into.
The board is exercised over its serial console and/or xsdb over JTAG, not a libIIO URI or SSH.
You want board launch (acquire + boot) to be a separate step from the test run, but within one job so the place reservation is held for the whole run with no cross-job state handoff.
The launch still reuses the existing discovery tooling
(adi-lg-hw-ci + the acquire-place action) and the adi-lg
boot commands — only the handoff differs.
Architecture¶
A single job runs these steps in order; the place is reserved from
acquire-place until the final release (which runs on
if: always()):
┌─ one GitHub Actions job (place held for the whole job) ───────┐
│ │
│ render-env adi-lg-hw-ci render-env → env.yaml │
│ │ │
│ acquire-place reserve + acquire the place via coordinator │
│ │ │
│ boot adi-lg boot-* --state shell (env.yaml) │
│ │ │
│ resolve adi-lg-hw-ci resolve-resources → LG_* vars │
│ │ (UART device/host:port, JTAG xsdb+targets)│
│ test ./test/run_hw.sh (reads LG_* env vars) │
│ │ │
│ release labgrid-client release (if: always()) │
└───────────────────────────────────────────────────────────────┘
Exported variables¶
adi-lg-hw-ci resolve-resources reads the booted target and emits
KEY=VALUE lines. Only the variables that apply to the place are
emitted — a network-only console produces no LG_UART_DEVICE, a
board with no JTAG produces no LG_JTAG_*.
Variable |
Source resource / attribute |
Notes |
|---|---|---|
|
|
A local |
|
|
Coordinator host proxying the console. Set on the
|
|
|
TCP socket port on |
|
serial |
Baud rate, if the resource declares one. |
|
|
Absolute path to |
|
|
JTAG target index for the root device (from xsdb |
|
|
JTAG target index for the MicroBlaze / processor core. |
Note
The console arrives via RemotePlace on the coordinator path, so
it is usually a NetworkSerialPort — you get LG_UART_HOST +
LG_UART_PORT, not LG_UART_DEVICE. A runner-local exporter
that exposes a raw /dev/ttyUSBx gets LG_UART_DEVICE instead.
Exactly one of the two shapes is populated.
Important
There is no JTAG cable-serial in the ADI resource set —
XilinxDeviceJTAG carries only target indices, and xsdb
auto-selects the cable on connect. The JTAG handoff therefore
exports the xsdb path plus the target indices (what a bash
xsdb invocation actually needs), and nothing more. Don’t expect a
LG_JTAG_SERIAL.
Reaching the board from bash¶
Network console (
LG_UART_HOST/LG_UART_PORT): connect to the TCP socket (e.g.microcom/telnettohost:port), or uselabgrid-client -p "$LG_PLACE" consoleif labgrid-client is on the runner.Local console (
LG_UART_DEVICE): open the/dev/ttyUSBxpath directly.JTAG: invoke
"$LG_JTAG_XSDB"with the exported target indices. Raw JTAG access typically requires the harness to run on the exporter host (where the blaster andxsdblive), not on an arbitrary runner.
CLI reference¶
adi-lg-hw-ci resolve-resources --config <env.yaml> [--target main] [--out stdout|github]
--configThe rendered env yaml that
render-envwrote andadi-lg boot-*booted against.resolve-resourcesdoes not re-query the coordinator — the place is already acquired and the env file is the source of truth.--targetTarget name within the env yaml. Default:
main.--outstdout(default) prints theKEY=VALUElines.githubalso appends them to$GITHUB_OUTPUT(and warns to stderr if that is unset). Lines are always printed to stdout so the command is usable outside GitHub Actions.
Worked example¶
A single-job workflow: render the env, acquire the place, boot to a Linux shell, resolve the UART/JTAG facts into the job environment, run the bash test driver, and release the place unconditionally.
name: HW bash test (UART + JTAG)
on: [workflow_dispatch]
jobs:
bash-hw:
runs-on: [self-hosted, hw-mini2]
env:
LG_COORDINATOR: 10.0.0.41:20408
LG_PLACE: mini2
VENV: ${{ runner.temp }}/hw-ci/venv
steps:
- uses: actions/checkout@v4
- name: Render env yaml from place tags
id: render
run: |
OUT="${{ runner.temp }}/env-${LG_PLACE}.yaml"
"$VENV/bin/adi-lg-hw-ci" render-env \
--coord "$LG_COORDINATOR" --place "$LG_PLACE" --out "$OUT"
echo "env_path=$OUT" >> "$GITHUB_OUTPUT"
- name: Acquire place
uses: tfcollins/labgrid-plugins/.github/actions/acquire-place@main
with:
coordinator: ${{ env.LG_COORDINATOR }}
place: ${{ env.LG_PLACE }}
labgrid_client: ${{ env.VENV }}/bin/labgrid-client
- name: Boot to shell
run: |
"$VENV/bin/adi-lg" boot-soc \
-c "${{ steps.render.outputs.env_path }}" --state shell
- name: Resolve UART + JTAG into the job environment
run: |
# --out github appends KEY=VALUE to $GITHUB_OUTPUT; the
# redirect mirrors the same lines into $GITHUB_ENV so later
# steps see them as plain environment variables.
"$VENV/bin/adi-lg-hw-ci" resolve-resources \
--config "${{ steps.render.outputs.env_path }}" \
--out github >> "$GITHUB_ENV"
- name: Run bash test driver
run: ./test/run_hw.sh # reads $LG_UART_HOST / $LG_JTAG_XSDB / …
- name: Release place
if: always()
run: |
LG="$VENV/bin/labgrid-client"
"$LG" -x "$LG_COORDINATOR" -p "$LG_PLACE" release || true
"$LG" -x "$LG_COORDINATOR" cancel-reservation --all 2>/dev/null || true
Note
acquire-place is a composite action and cannot define a
post: cleanup, so the unconditional Release place step is
required — without it a killed runner leaves the place held. This
mirrors the release idiom in hw-matrix.yml.
See also¶
Hardware CI v2 (discovery-driven) — the pytest-driven discovery flow (use that when your tests are Python, not bash).
Using Drivers → CloudsmithDLDriver and Command Line Interface →
cloudsmithdl— if theadi-lg boot-*step needs boot artifacts (e.g.BOOT.BIN) fetched from a Cloudsmith repo, thekuiperbinding on the SoC boot strategies acceptsCloudsmithDLDriverin place ofKuiperDLDriver.Hardware CI (v1, manifest-first) → Carrier-keyed nightly dispatch — the coordinator-leg path that fans a single
ci/hardware_targets.ymlentry out to every place of a carrier (e.g. the zc706 boot-to-shell job). Use that when a pytest boot-to-shell check fits better than a bash UART/JTAG harness.