Skip to content

Portable Physical Lab CI

Use this workflow pack when a CI runner can reach real Android devices through adb and you want the same Luotsi command path in GitHub Actions, another Actions-compatible runner, or a generic CI job.

The design has two layers:

  • portable scripts under eng/ci/ that define the CI-neutral contract
  • a GitHub Actions workflow adapter that maps workflow inputs to those scripts

The reusable entry points are:

Terminal window
bash ./eng/ci/run-lab-scenarios.sh
Terminal window
.\eng\ci\run-lab-scenarios.ps1

Both scripts read the same environment variables.

| Variable | Default | Purpose | | --- | --- | --- | | LUOTSI_DEVICE_QUERY | state=online,type=physical,availability=available | Lab selector passed to lab status, lab plan, and run. | | LUOTSI_SCENARIO_PATH | examples/scenarios | Scenario file or directory. | | LUOTSI_OWNER | CI-derived owner, or ci-local | Device lease owner. | | LUOTSI_TTL_SEC | 1800 | Device claim lifetime. | | LUOTSI_ARTIFACTS_DIR | artifacts/luotsi-lab | Artifact root to upload from CI. | | LUOTSI_JUNIT_PATH | <artifacts>/junit.xml | JUnit report path. | | LUOTSI_BIN | luotsi | Luotsi executable. | | LUOTSI_DRY_RUN | false | Validate scripts and scenarios without claiming a device. |

Normal execution validates scenarios, explains the selected lab target, claims one device for the run, writes JUnit plus Luotsi artifacts, and persists a replay run summary packet.

The GitHub adapter uses a self-hosted runner selected by labels:

runs-on: [self-hosted, luotsi-lab, android-device]

The checked-in GitHub Actions adapter is .github/workflows/android-lab-scenarios.yml.

It is manual by default. It accepts device_query, scenario_path, ttl_sec, and dry_run inputs, then calls the Bash script on POSIX runners or the PowerShell script on Windows runners.

Recommended first-run inputs:

device_query: state=online,type=physical,availability=available
scenario_path: examples/scenarios
ttl_sec: 1800
dry_run: true

Switch dry_run to false only after the runner host can run:

Terminal window
luotsi version
luotsi devices
luotsi lab status --device-query state=online,type=physical,availability=available

Use runner labels or runner groups to restrict access to trusted repositories. Avoid running physical-lab workflows from untrusted pull requests unless the runner is disposable and isolated.

The workflow also fixes LUOTSI_ARTIFACTS_DIR to artifacts/luotsi-lab, fixes LUOTSI_JUNIT_PATH to artifacts/luotsi-lab/junit.xml, uploads that directory as luotsi-lab-<run_id>-<run_attempt> after the job completes, and lets the scripts append run-summary.md to the GitHub Actions job summary when replay metadata is available.

The portable scripts and .github/workflows/android-lab-scenarios.yml intentionally keep a narrow contract today: they surface device_query, scenario_path, ttl_sec, and dry_run, but do not yet surface --claim-wait-sec, --device-pool, or --require-capabilities.

That means the reusable pack is a good fit when live adb state plus --device-query is enough to select hardware. If you need the durable scheduler queue or durable inventory admission, call Luotsi directly in the CI job instead of the helper script.

For example:

Terminal window
luotsi lab plan --device-query "state=online,type=physical,availability=available,model=Pixel_9" --device-pool checkout --require-capabilities camera,nfc
luotsi run --path examples/scenarios --device-query "state=online,type=physical,availability=available,model=Pixel_9" --claim-device --claim-wait-sec 300 --device-pool checkout --require-capabilities camera,nfc --owner "gh-actions-${GITHUB_RUN_ID:-local}" --ttl-sec 1800 --report-junit artifacts/luotsi-lab/junit.xml --artifacts artifacts/luotsi-lab
luotsi replay packet --artifacts artifacts/luotsi-lab

replay packet is the CI handoff writer: it reports the At a Glance summary, failure snapshot, focused evidence files, recommended next action, 60-second checklist, and follow-up replay commands without trying to launch a browser on the runner. It also writes run-summary.json and run-summary.md, which are the first files to inspect from an uploaded lab artifact bundle. Use replay open --dry-run --write-json --write-markdown when a human also wants the replay front-door response.

The helper scripts preserve the scenario run exit code, but still attempt replay packet, replay packet --check, and the run-summary.md GitHub job-summary append after a failing run. The job stays red with the scenario run exit code, and the uploaded artifact bundle still has the first-minute packet. If packet writing or validation fails before run-summary.md exists, the scripts still append a fallback summary with exact replay packet and replay packet --check commands for the uploaded artifact root.

Use that explicit form when the job must wait fairly for a leased device or when allocation has to respect durable pool and capability registration.

For CI systems that are not GitHub Actions, keep the same environment variables and upload LUOTSI_ARTIFACTS_DIR as the job artifact.

Terminal window
export LUOTSI_DEVICE_QUERY="state=online,type=physical,availability=available,model=Pixel_9"
export LUOTSI_SCENARIO_PATH="examples/scenarios"
export LUOTSI_OWNER="${CI_JOB_ID:-local-lab}"
export LUOTSI_ARTIFACTS_DIR="artifacts/luotsi-lab"
export LUOTSI_JUNIT_PATH="artifacts/luotsi-lab/junit.xml"
bash ./eng/ci/run-lab-scenarios.sh

Run a device-free integration check with:

Terminal window
LUOTSI_DRY_RUN=true bash ./eng/ci/run-lab-scenarios.sh

When the generic CI job needs --claim-wait-sec, --device-pool, or --require-capabilities, bypass the helper script and call luotsi lab plan, luotsi run, and luotsi replay packet directly so the job contract stays explicit.

The recommended Docker model is host ADB. Put Luotsi and the scripts inside the container, but connect to a host adb server or TCP-visible Android devices instead of relying on privileged USB passthrough.

Mount the repository or scenario files, mount an artifact output directory, and pass the same LUOTSI_* variables. If the container uses the host adb server, configure the adb client for that topology, for example with ADB_SERVER_SOCKET or a host name such as host.docker.internal.

Privileged USB passthrough is possible on some lab hosts, but it is an advanced fallback because it depends on host USB, udev, permissions, and container runtime behavior. Keep device ownership in Luotsi leases either way.