From ceb6d752a44bad44ae0939ef2f3ddeae46adb948 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Wed, 10 Jun 2026 17:15:59 -0400 Subject: [PATCH 1/7] ci: vendor stdlib Prism uploader (private repo; same script as pyadi-dt) --- .github/scripts/_prism_client.py | 190 ++++++++++++++++++++ .github/scripts/prism_upload_run.py | 266 ++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 .github/scripts/_prism_client.py create mode 100755 .github/scripts/prism_upload_run.py diff --git a/.github/scripts/_prism_client.py b/.github/scripts/_prism_client.py new file mode 100644 index 00000000..02577d7b --- /dev/null +++ b/.github/scripts/_prism_client.py @@ -0,0 +1,190 @@ +"""Stdlib-only HTTP client for the Prism API. + +Shared by the other scripts in this directory (`seed_demo.py`, +`upload_run.py`) so the login + CSRF + multipart dance lives in exactly +one place. No external dependencies — works anywhere Python 3.10+ is +installed (CI runners included). + +Session state (auth + CSRF cookies) is held in an in-memory cookie jar +so all subsequent `self._request` calls automatically carry the login +cookie and, for mutations, the `X-Prism-Csrf` header. +""" + +from __future__ import annotations + +import http.cookiejar +import json +import urllib.error +import urllib.parse +import urllib.request +import uuid + +__all__ = ["PrismClient"] + + +class PrismClient: + def __init__(self, base_url: str) -> None: + # Reject non-http(s) schemes up front so the noqa: S310 below is true. + # urllib's default opener handles file://, ftp://, data://, etc., which + # an attacker who can set PRISM_URL could abuse for local-file + # disclosure / SSRF. See ruff S310. This guard must mirror the one in + # clients/python-pytest/src/pytest_prism/client.py since this file is + # the vendoring source for pyadi-iio's prism-report plugin. + scheme = urllib.parse.urlsplit(base_url).scheme.lower() + if scheme not in ("http", "https"): + raise ValueError( + f"PrismClient base_url must be http or https; got scheme {scheme!r}" + ) + self.base_url = base_url.rstrip("/") + self.jar = http.cookiejar.CookieJar() + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(self.jar) + ) + + # --------------------------------------------------------------------- # + # Low-level request plumbing + # --------------------------------------------------------------------- # + + def _read_cookie(self, name: str) -> str | None: + for c in self.jar: + if c.name == name: + return c.value + return None + + def _request( + self, + method: str, + path: str, + *, + body: bytes | None = None, + headers: dict[str, str] | None = None, + ) -> tuple[int, bytes]: + url = f"{self.base_url}{path}" + # base_url's scheme is validated to http/https in __init__, so this is safe. + req = urllib.request.Request( + url, data=body, method=method, headers=headers or {} + ) # noqa: S310 + csrf = self._read_cookie("prism_csrf") + if csrf and method in ("POST", "PUT", "PATCH", "DELETE"): + req.add_header("X-Prism-Csrf", csrf) + try: + with self.opener.open(req) as resp: + return resp.status, resp.read() + except urllib.error.HTTPError as exc: + return exc.code, exc.read() + + # --------------------------------------------------------------------- # + # Auth + # --------------------------------------------------------------------- # + + def login(self, email: str, password: str) -> None: + payload = json.dumps({"email": email, "password": password}).encode("utf-8") + code, body = self._request( + "POST", + "/api/v1/auth/login", + body=payload, + headers={"Content-Type": "application/json"}, + ) + if code != 200: + raise RuntimeError(f"login failed: HTTP {code} {body!r}") + + # --------------------------------------------------------------------- # + # Projects + # --------------------------------------------------------------------- # + + def ensure_project(self, slug: str, name: str, description: str = "") -> None: + """Create the project if it doesn't exist. 409 (already exists) is fine.""" + payload = json.dumps( + {"slug": slug, "name": name, "description": description} + ).encode("utf-8") + code, body = self._request( + "POST", + "/api/v1/projects", + body=payload, + headers={"Content-Type": "application/json"}, + ) + if code not in (201, 409): + raise RuntimeError(f"project create failed: HTTP {code} {body!r}") + + def project_exists(self, slug: str) -> bool: + code, _ = self._request("GET", f"/api/v1/projects/{urllib.parse.quote(slug)}") + return code == 200 + + # --------------------------------------------------------------------- # + # Runs + # --------------------------------------------------------------------- # + + def list_runs(self, project_slug: str) -> list[dict[str, object]]: + code, body = self._request( + "GET", f"/api/v1/runs?project={urllib.parse.quote(project_slug)}" + ) + if code != 200: + raise RuntimeError(f"list runs failed: HTTP {code} {body!r}") + result: list[dict[str, object]] = json.loads(body) + return result + + def get_run(self, run_id: str) -> dict[str, object]: + code, body = self._request("GET", f"/api/v1/runs/{urllib.parse.quote(run_id)}") + if code != 200: + raise RuntimeError(f"get run failed: HTTP {code} {body!r}") + result: dict[str, object] = json.loads(body) + return result + + def delete_run(self, run_id: str) -> None: + code, body = self._request( + "DELETE", f"/api/v1/runs/{urllib.parse.quote(run_id)}" + ) + if code not in (204, 404): + raise RuntimeError(f"delete run failed: HTTP {code} {body!r}") + + def upload_run( + self, + *, + project_slug: str, + run_name: str, + junit_xml: bytes, + archive_zip: bytes | None = None, + tags: dict[str, str] | None = None, + ) -> dict[str, object]: + """POST /api/v1/runs with a hand-built multipart body. + + Returns the parsed JSON response (includes run `id` + initial `status`). + Raises RuntimeError on any non-201. + """ + boundary = f"----prism{uuid.uuid4().hex}" + parts: list[bytes] = [] + + def _add_field(name: str, value: str) -> None: + parts.append(f"--{boundary}\r\n".encode()) + parts.append( + f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode() + ) + parts.append(value.encode("utf-8")) + parts.append(b"\r\n") + + def _add_file( + name: str, filename: str, content_type: str, content: bytes + ) -> None: + parts.append(f"--{boundary}\r\n".encode()) + parts.append( + f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'.encode() + ) + parts.append(f"Content-Type: {content_type}\r\n\r\n".encode()) + parts.append(content) + parts.append(b"\r\n") + + metadata = {"project_slug": project_slug, "name": run_name, "tags": tags or {}} + _add_field("metadata", json.dumps(metadata)) + _add_file("junit", "junit.xml", "application/xml", junit_xml) + if archive_zip is not None: + _add_file("archive", "archive.zip", "application/zip", archive_zip) + parts.append(f"--{boundary}--\r\n".encode()) + body = b"".join(parts) + headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"} + code, resp_body = self._request( + "POST", "/api/v1/runs", body=body, headers=headers + ) + if code != 201: + raise RuntimeError(f"upload {run_name!r} failed: HTTP {code} {resp_body!r}") + result: dict[str, object] = json.loads(resp_body) + return result diff --git a/.github/scripts/prism_upload_run.py b/.github/scripts/prism_upload_run.py new file mode 100755 index 00000000..4860ccce --- /dev/null +++ b/.github/scripts/prism_upload_run.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Upload a pytest JUnit XML result to a Prism server. + +Typical use from CI: + + python3 scripts/upload_run.py results.xml \\ + --project my-service --run-name "$CI_JOB_ID" \\ + --tag branch=$GIT_BRANCH --tag sha=$GIT_SHA --wait + +All `--foo` flags fall back to `PRISM_FOO`-style environment variables, +so a hardened CI pipeline can skip the command-line switches entirely: + + PRISM_URL=... PRISM_EMAIL=... PRISM_PASSWORD=... \\ + PRISM_PROJECT=my-service PRISM_RUN_NAME="$CI_JOB_ID" \\ + python3 scripts/upload_run.py results.xml + +Exit codes: + 0 success + 2 bad argument or input file not found + 3 authentication failed + 4 project not found (pass --auto-create-project to create it) + 5 upload failed (HTTP error from the server) + 6 --wait timed out before ingest finished + +Stdlib-only — works on any CI runner with Python 3.10+. +""" + +from __future__ import annotations + +import argparse +import os +import sys +import time +from pathlib import Path + +# The shared client lives next to this file; use a path-relative import so the +# script works when invoked from anywhere (e.g. `python3 /repo/scripts/...`). +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from _prism_client import PrismClient # noqa: E402 (import after sys.path tweak) + +EXIT_OK = 0 +EXIT_BAD_INPUT = 2 +EXIT_AUTH = 3 +EXIT_NO_PROJECT = 4 +EXIT_UPLOAD = 5 +EXIT_WAIT_TIMEOUT = 6 + + +def _parse_tag(raw: str) -> tuple[str, str]: + if "=" not in raw: + raise argparse.ArgumentTypeError(f"--tag expects key=value, got {raw!r}") + k, v = raw.split("=", 1) + k = k.strip() + v = v.strip() + if not k: + raise argparse.ArgumentTypeError(f"--tag key is empty in {raw!r}") + return k, v + + +def _env(name: str, default: str | None = None) -> str | None: + v = os.environ.get(name) + return v if v else default + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="upload_run.py", + description="Upload a pytest JUnit XML to Prism.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "Each --foo flag also accepts a PRISM_FOO environment variable " + "(e.g. PRISM_URL, PRISM_EMAIL, PRISM_PROJECT, PRISM_RUN_NAME)." + ), + ) + p.add_argument("junit", type=Path, help="Path to the JUnit XML file.") + p.add_argument( + "--url", + default=_env("PRISM_URL", "http://localhost:8000"), + help="Prism API base URL (env: PRISM_URL).", + ) + p.add_argument( + "--email", default=_env("PRISM_EMAIL"), help="Login email (env: PRISM_EMAIL)." + ) + p.add_argument( + "--password", + default=_env("PRISM_PASSWORD"), + help="Login password (env: PRISM_PASSWORD).", + ) + p.add_argument( + "--project", + default=_env("PRISM_PROJECT"), + help="Target project slug (env: PRISM_PROJECT).", + ) + p.add_argument( + "--run-name", + dest="run_name", + default=_env("PRISM_RUN_NAME"), + help="Name for this Test Suite Run (env: PRISM_RUN_NAME).", + ) + p.add_argument( + "--tag", + action="append", + type=_parse_tag, + default=[], + metavar="key=value", + help="Repeatable. Arbitrary tag on the run (branch=main, sha=abc123).", + ) + p.add_argument( + "--archive", + type=Path, + default=None, + help="Optional .zip of measurement artifacts to upload alongside the JUnit.", + ) + p.add_argument( + "--auto-create-project", + action="store_true", + help="Create the target project if it doesn't exist.", + ) + p.add_argument( + "--wait", + nargs="?", + type=int, + const=60, + default=None, + metavar="SECONDS", + help="After upload, poll every 2s until the run is no longer " + "pending. Bare flag uses a 60s timeout; pass a value for longer.", + ) + verbosity = p.add_mutually_exclusive_group() + verbosity.add_argument( + "--quiet", + "-q", + action="store_true", + help="Only print the final result line; errors still go to stderr.", + ) + verbosity.add_argument( + "--verbose", "-v", action="store_true", help="Print each step to stdout." + ) + return p + + +def _require(args: argparse.Namespace, name: str, env: str) -> str | None: + val: str | None = getattr(args, name, None) + if not val: + print( + f"error: --{name.replace('_', '-')} is required (or set {env})", + file=sys.stderr, + ) + return None + return val + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + + # --- Validate required args ------------------------------------------- # + missing = [] + for attr, env in ( + ("email", "PRISM_EMAIL"), + ("password", "PRISM_PASSWORD"), + ("project", "PRISM_PROJECT"), + ("run_name", "PRISM_RUN_NAME"), + ): + if not getattr(args, attr, None): + missing.append(f"--{attr.replace('_', '-')} (or {env})") + if missing: + print( + "error: missing required argument(s): " + ", ".join(missing), + file=sys.stderr, + ) + return EXIT_BAD_INPUT + + if not args.junit.exists() or not args.junit.is_file(): + print(f"error: JUnit file not found: {args.junit}", file=sys.stderr) + return EXIT_BAD_INPUT + if args.archive is not None and ( + not args.archive.exists() or not args.archive.is_file() + ): + print(f"error: archive file not found: {args.archive}", file=sys.stderr) + return EXIT_BAD_INPUT + + say = (lambda _m: None) if args.quiet else print + + # --- Auth ------------------------------------------------------------- # + client = PrismClient(args.url) + if args.verbose: + say(f"→ logging in to {args.url} as {args.email}") + try: + client.login(args.email, args.password) + except RuntimeError as exc: + print(f"error: authentication failed — {exc}", file=sys.stderr) + return EXIT_AUTH + + # --- Project ---------------------------------------------------------- # + if args.auto_create_project: + client.ensure_project( + args.project, args.project, description="Auto-created by upload_run.py" + ) + elif not client.project_exists(args.project): + print( + f"error: project {args.project!r} not found. " + "Pass --auto-create-project to create it, or create it in the UI first.", + file=sys.stderr, + ) + return EXIT_NO_PROJECT + + # --- Read payloads ---------------------------------------------------- # + junit_bytes = args.junit.read_bytes() + archive_bytes = args.archive.read_bytes() if args.archive is not None else None + tags = dict(args.tag) + + if args.verbose: + say( + f"→ uploading {args.junit.name} ({len(junit_bytes)} bytes) " + f"as run {args.run_name!r} in project {args.project!r}" + + ( + f" with archive {args.archive.name} ({len(archive_bytes or b'')} bytes)" + if archive_bytes + else "" + ) + ) + + # --- Upload ----------------------------------------------------------- # + try: + result = client.upload_run( + project_slug=args.project, + run_name=args.run_name, + junit_xml=junit_bytes, + archive_zip=archive_bytes, + tags=tags, + ) + except RuntimeError as exc: + print(f"error: upload failed — {exc}", file=sys.stderr) + return EXIT_UPLOAD + + run_id: str = str(result["id"]) + status: str = str(result.get("status", "pending")) + + # --- Optional wait-for-ingest ---------------------------------------- # + if args.wait is not None: + if args.verbose: + say(f"→ waiting up to {args.wait}s for ingest to finish …") + deadline = time.monotonic() + args.wait + while status == "pending": + if time.monotonic() >= deadline: + print( + f"warning: timed out after {args.wait}s; run {run_id} is still pending", + file=sys.stderr, + ) + # Still print the normal success line so CI can capture the ID. + print(f"uploaded {args.run_name} (id={run_id}, status=pending)") + return EXIT_WAIT_TIMEOUT + time.sleep(2) + try: + detail = client.get_run(run_id) + except RuntimeError as exc: + print(f"warning: could not poll run status — {exc}", file=sys.stderr) + break + status = str(detail.get("status", status)) + + print(f"uploaded {args.run_name} (id={run_id}, status={status})") + return EXIT_OK + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From e7e71489b8db1102bce59300f9e1e19279ca60eb Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Wed, 10 Jun 2026 17:16:23 -0400 Subject: [PATCH 2/7] ci: add matlab-hw-request@v3 caller beside bespoke hw-matlab (staggered cron) --- .github/workflows/hw-matlab-request.yml | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/hw-matlab-request.yml diff --git a/.github/workflows/hw-matlab-request.yml b/.github/workflows/hw-matlab-request.yml new file mode 100644 index 00000000..8ebfa1ac --- /dev/null +++ b/.github/workflows/hw-matlab-request.yml @@ -0,0 +1,46 @@ +name: Hardware Tests (matlab-hw-request v3) + +# Successor to the bespoke hw-matlab.yml, running side-by-side during the +# migration window (staggered cron). Discovery intersects +# test/hw_ci/board_map.yaml with live coordinator places; each leg boots +# via the place's boot-strategy tag (iiod-verified, bounded retry), runs +# runHWTests() against $IIO_URI, and parses the JUnit for +# pass/fail (MATLAB exit-code quirks tolerated, all-skipped = success). + +permissions: + contents: read + checks: write + pull-requests: write + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + types: [labeled, opened, synchronize, reopened] + schedule: + - cron: "0 5 * * *" # bespoke runs at 08:00; pyadi repos take 04:00/04:30 + +jobs: + hw-matlab: + if: >- + github.event_name != 'pull_request' || + contains(github.event.pull_request.labels.*.name, 'hw-test') + uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3 + with: + coordinator: ${{ vars.LG_COORDINATOR }} # gRPC :20408, NOT :8000 + board-map: "test/hw_ci/board_map.yaml" + runner-label: ${{ vars.HW_REQUEST_RUNNER }} + preflight-runner-label: ${{ vars.HW_PREFLIGHT_RUNNER }} + matlab-bin: ${{ vars.MATLAB_BIN }} + prism-upload: ${{ vars.PRISM_UPLOAD_ENABLED == 'true' }} + prism-project: transceiver-toolbox + prism-upload-cmd: >- + python3 .github/scripts/prism_upload_run.py "$PRISM_JUNIT" + --url "$PRISM_URL" --project "$PRISM_PROJECT" --run-name "$PRISM_RUN_NAME" + --auto-create-project + --tag "board=$PRISM_BOARD" --tag "carrier=$PRISM_CARRIER" + --tag "place=$PRISM_PLACE" --tag "sha=$GITHUB_SHA" + secrets: + PRISM_EMAIL: ${{ secrets.PRISM_EMAIL }} + PRISM_PASSWORD: ${{ secrets.PRISM_PASSWORD }} From 54a73b8a3681ef4e350fbfcf8ee2d01ea7b916cf Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Wed, 10 Jun 2026 17:17:18 -0400 Subject: [PATCH 3/7] doc: hardware CI via matlab-hw-request@v3 (board map, triggers, local repro) --- README.md | 51 ++++++++++++++++++++++++++------------- test/hw_ci/board_map.yaml | 8 +++--- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ceb2e289..cd5ed1d6 100644 --- a/README.md +++ b/README.md @@ -43,29 +43,46 @@ and honor the `IIO_URI` environment variable. They assume the board is already powered, booted, and reachable. [adi-labgrid-plugins](https://github.com/analogdevicesinc/adi-labgrid-plugins) -can provision that board automatically — power it, boot the FPGA/SoC, and hand -MATLAB the booted board's URI — both locally and in GitHub Actions, via the -`adi-lg-matlab` launcher. +provisions that board automatically — power, boot the FPGA/SoC, verify iiod +is up, hand MATLAB the URI — both locally and in GitHub Actions. -The mapping from a labgrid place (tagged with `carrier` / `daughter-board`) to -the MATLAB board reference name that `runHWTests` expects lives in +### CI triggers + +Hardware CI runs via +[`.github/workflows/hw-matlab-request.yml`](.github/workflows/hw-matlab-request.yml) +(reusable `matlab-hw-request.yml@v3`): + +- **Nightly** — `0 5 * * *` UTC (offset from bespoke `hw-matlab.yml` at 08:00). +- **Push to `master`**. +- **`workflow_dispatch`** — manual trigger from the Actions UI. +- **Pull request** — only when the PR carries the `hw-test` label (add it to + request a hardware run on your branch). + +Both `hw-matlab.yml` (legacy) and `hw-matlab-request.yml` (v3) run in parallel +during the migration window. + +### Board map + +The mapping from a labgrid coordinator place (tagged `carrier` / +`daughter-board` / `hdl-config`) to the MATLAB board string that +`runHWTests(board)` expects lives in [`test/hw_ci/board_map.yaml`](test/hw_ci/board_map.yaml). +The most-specific matching row wins; a row without `carrier` is a +carrier-agnostic fallback. Keep this file in sync with the `switch` in +`test/runHWTests.m` when adding boards. -Run locally against a coordinator place: +### Local repro -```bash -pip install "adi-labgrid-plugins @ git+https://github.com/tfcollins/labgrid-plugins.git@v2" +Request a board from the coordinator, boot it, and run the tests locally: -adi-lg-matlab run \ - --coord $LG_COORDINATOR --place mini2 \ - --board-map test/hw_ci/board_map.yaml \ - --repo-dir . --matlab /opt/MATLAB/R2025b/bin/matlab \ - --junit junit-mini2.xml --acquire +```bash +adi-lg request --part --carrier --wait 300 \ + --run "matlab -batch \"addpath(genpath('hdl')); addpath(genpath('test')); runHWTests('')\"" ``` -This boots the board, sets `IIO_URI`, runs `runHWTests`, copies the JUnit -results, and releases the place. The GitHub Actions equivalent is -[`.github/workflows/hw-matlab.yml`](.github/workflows/hw-matlab.yml). See the +Replace ``, ``, and `` with values from +`test/hw_ci/board_map.yaml` (e.g. `adrv9009`, `zcu102`, +`zynqmp-zcu102-rev10-adrv9009`). See the [MATLAB Hardware CI guide](https://adi-labgrid-plugins.readthedocs.io/en/latest/user-guide/matlab-hw-ci.html) -for details. +for full coordinator setup details. diff --git a/test/hw_ci/board_map.yaml b/test/hw_ci/board_map.yaml index 58bc0ea6..99e28ee7 100644 --- a/test/hw_ci/board_map.yaml +++ b/test/hw_ci/board_map.yaml @@ -1,11 +1,11 @@ -# TransceiverToolbox board map for labgrid HW-CI (adi-lg-matlab). +# TransceiverToolbox board map for labgrid HW-CI (matlab-hw-request.yml@v3 / adi-lg-hw-ci matlab-matrix). # # Maps a labgrid coordinator place's tags -> the MATLAB board reference # name that `runHWTests(board)` understands (the values in the `switch` -# in test/runHWTests.m). adi-lg-matlab uses this to translate a booted -# place into the right `board` argument. +# in test/runHWTests.m). matlab-hw-request.yml@v3 uses this to translate +# a booted place into the right `board` argument. # -# Matching rules (see adi_lg_plugins.matlab_ci.board_map): +# Matching rules (see adi_lg_plugins.hw_ci.board_map): # * `daughter-board` is required and matched against the place's # `daughter-board` tag. # * `carrier` / `hdl-config`, when present, must also match the place's From 70c12f526bfadcdf9d33bc34a348905b9a65ab44 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Thu, 11 Jun 2026 20:08:23 -0400 Subject: [PATCH 4/7] ci: bump to labgrid-plugins v3.2 (coordinator robustness fixes) --- .github/workflows/hw-matlab-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw-matlab-request.yml b/.github/workflows/hw-matlab-request.yml index 8ebfa1ac..e32313eb 100644 --- a/.github/workflows/hw-matlab-request.yml +++ b/.github/workflows/hw-matlab-request.yml @@ -1,4 +1,4 @@ -name: Hardware Tests (matlab-hw-request v3) +name: Hardware Tests (matlab-hw-request v3.2) # Successor to the bespoke hw-matlab.yml, running side-by-side during the # migration window (staggered cron). Discovery intersects @@ -26,7 +26,7 @@ jobs: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') - uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3 + uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.2 with: coordinator: ${{ vars.LG_COORDINATOR }} # gRPC :20408, NOT :8000 board-map: "test/hw_ci/board_map.yaml" From d6e794a19d8f52d024b4c45a28082dda99b1e917 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 12 Jun 2026 00:14:39 -0400 Subject: [PATCH 5/7] ci: bump to labgrid-plugins v3.3 (DHCP-race fix in URI resolution) --- .github/workflows/hw-matlab-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw-matlab-request.yml b/.github/workflows/hw-matlab-request.yml index e32313eb..2fe5b49d 100644 --- a/.github/workflows/hw-matlab-request.yml +++ b/.github/workflows/hw-matlab-request.yml @@ -1,4 +1,4 @@ -name: Hardware Tests (matlab-hw-request v3.2) +name: Hardware Tests (matlab-hw-request v3.3) # Successor to the bespoke hw-matlab.yml, running side-by-side during the # migration window (staggered cron). Discovery intersects @@ -26,7 +26,7 @@ jobs: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') - uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.2 + uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.3 with: coordinator: ${{ vars.LG_COORDINATOR }} # gRPC :20408, NOT :8000 board-map: "test/hw_ci/board_map.yaml" From fa17ec050cf7b74a73e047aa3f8b5c180e4a2f5c Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 12 Jun 2026 09:30:46 -0400 Subject: [PATCH 6/7] ci: bump to labgrid-plugins v3.4 (lg_feature parity in rendered envs) --- .github/workflows/hw-matlab-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw-matlab-request.yml b/.github/workflows/hw-matlab-request.yml index 2fe5b49d..0ac2c54e 100644 --- a/.github/workflows/hw-matlab-request.yml +++ b/.github/workflows/hw-matlab-request.yml @@ -1,4 +1,4 @@ -name: Hardware Tests (matlab-hw-request v3.3) +name: Hardware Tests (matlab-hw-request v3.4) # Successor to the bespoke hw-matlab.yml, running side-by-side during the # migration window (staggered cron). Discovery intersects @@ -26,7 +26,7 @@ jobs: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') - uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.3 + uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.4 with: coordinator: ${{ vars.LG_COORDINATOR }} # gRPC :20408, NOT :8000 board-map: "test/hw_ci/board_map.yaml" From 6a03b90ade4fc843ea59a3c2a16b2ef392329b30 Mon Sep 17 00:00:00 2001 From: "Travis F. Collins" Date: Fri, 12 Jun 2026 10:55:27 -0400 Subject: [PATCH 7/7] ci: bump to labgrid-plugins v3.5 (stable per-place MACs on TFTP boot) --- .github/workflows/hw-matlab-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hw-matlab-request.yml b/.github/workflows/hw-matlab-request.yml index 0ac2c54e..37a3dbf9 100644 --- a/.github/workflows/hw-matlab-request.yml +++ b/.github/workflows/hw-matlab-request.yml @@ -1,4 +1,4 @@ -name: Hardware Tests (matlab-hw-request v3.4) +name: Hardware Tests (matlab-hw-request v3.5) # Successor to the bespoke hw-matlab.yml, running side-by-side during the # migration window (staggered cron). Discovery intersects @@ -26,7 +26,7 @@ jobs: if: >- github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'hw-test') - uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.4 + uses: tfcollins/labgrid-plugins/.github/workflows/matlab-hw-request.yml@v3.5 with: coordinator: ${{ vars.LG_COORDINATOR }} # gRPC :20408, NOT :8000 board-map: "test/hw_ci/board_map.yaml"