tvlabs execute
tvlabs execute is experimental. Its flags, the TVLABS_* environment variables it
sets, and its exit codes may all change without a major version bump, and it is not yet
covered by the deprecation policy the rest of the CLI follows.
Pin your CLI version if you depend on it from CI, and tell us what you need at [email protected] — feedback now is what settles the interface.
Running your own test harness against a real device
Use tvlabs execute to request a device, connect to it, and run a script of yours
against it. Where tvlabs run executes automation workflows you built in the
TV Labs web interface, execute runs your harness — Appium, WebDriver, Playwright,
a shell script, whatever your suite already uses — and gets out of the way.
tvlabs execute ./run_suite.sh --target 'platform_key:fire_tv AND year>=2023'
The command requests a device matching --target, waits for it to warm up, opens the
tunnel, then runs your entrypoint with environment variables describing the session.
Your entrypoint's exit code becomes the command's exit code. When it finishes, the
session is ended and the tunnel torn down.
Everything the command prints itself goes to stderr. Stdout belongs entirely to your
entrypoint, so a harness whose output is machine-read — TAP, JUnit on stdout, a
$(tvlabs execute ...) substitution — stays clean.
The entrypoint
The entrypoint is the first argument. It runs wherever the CLI runs — your shell, a CI runner, a container — not on the device. It reaches the device over the tunnel.
# Both spellings work; the positional form is shorter.
tvlabs execute ./run_suite.sh --target 'platform_key:roku'
tvlabs execute --entrypoint ./run_suite.sh --target 'platform_key:roku'
It is executed directly, without a shell, so it takes no arguments of its own — put what
your harness needs in --env or inside the script. Relative paths resolve against the
directory you ran the command from, not against --context.
Environment contract
Your entrypoint inherits the CLI's entire environment, plus these:
| Variable | Description |
|---|---|
TVLABS_SESSION_ID | The session's UUID |
TVLABS_SESSION_URL | Link to the session in the TV Labs web interface |
TVLABS_PLATFORM | Platform key of the matched device, e.g. fire_tv |
TVLABS_DEVICE_HOST | Host the tunnel is bound to (see --bind-address) |
TVLABS_INSPECTOR_PORT | Local port forwarded to the device's web inspector |
TVLABS_APP_PORT | Port of your locally running web app, forwarded to the device |
These six are reserved: passing any of them as --env is an error rather than a silent
override, since it would hand your harness a session it cannot reach.
Everything else passes straight through:
tvlabs execute ./run_suite.sh \
--target 'platform_key:tizen' \
--env SUITE=smoke \
--env RETRIES=2
Installing a build
Upload and install a build as part of device warmup with --build:
tvlabs execute ./run_suite.sh --target 'platform_key:tizen' --build ./app.wgt
To reuse a build you already uploaded with tvlabs upload, pass its id
instead. The two are mutually exclusive — a mistyped path fails immediately rather than
being sent to the server as a build id that doesn't exist.
tvlabs execute ./run_suite.sh --target 'platform_key:tizen' --build-id 408cb137-1b75-4322-b8f6-9eccf5b6b149
Exit codes
Built so a CI retry policy can tell your failures from ours:
| Code | Meaning |
|---|---|
| entrypoint's own | Your harness ran to completion. Its status is the command's status |
1 | A local error — bad flags, or a --target the server rejects |
124 | No device was provisioned and connected within --match-timeout |
125 | The platform failed before your entrypoint started. Safe to retry |
126 | The session died under a running entrypoint. Any results it published are partial |
130 / 143 | Interrupted by SIGINT / SIGTERM |
125 and 126 are the two worth wiring into a retry rule: 125 means nothing of yours
ran, so a retry costs nothing, while 126 means it ran against a device that
disappeared.
Options
| Flag | Default | Description |
|---|---|---|
--target | required | KQL device query |
--entrypoint | — | Script or binary to run. The positional argument is the shorter spelling |
--context | . | Working directory for the entrypoint |
--env | — | KEY=VALUE passed to the entrypoint. Repeatable |
--build | — | Path to a build file to upload and install during warmup |
--build-id | — | Existing build id to install during warmup |
--match-timeout | 30m | How long to wait for a device. Does not bound your entrypoint's run |
--inspector-port, -i | 9222 | Local port forwarded to the device's web inspector |
--app-port, -p | 50552 | Port of a locally running web application to forward to the device |
--bind-address, -b | localhost | Address to bind the tunnel to, and the TVLABS_DEVICE_HOST your entrypoint dials |
--match-timeout bounds provisioning only. Your entrypoint's own run is not bounded
here — a long one is bounded by the session's server-side timeout. Name your own timeout
inside the harness if you need one.
Signals and cleanup
Your entrypoint runs in its own process group, so a Ctrl-C or a CI job cancellation
reaches the whole tree — including anything your harness spawned — rather than orphaning
children against a device whose session is about to end. It gets SIGTERM first with a
grace period to flush results, then SIGKILL.
Because it is a background process group, an interactive read from a terminal would stop it. Stdin is therefore passed through when it is a pipe or a file — so a CI job feeding its harness on stdin keeps working — and detached when it is a terminal.
GitHub Actions example
name: TV Device Tests
on: [push, pull_request]
jobs:
device-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install TV Labs CLI
run: /bin/bash -c "$(curl -fsSL https://tvlabs.ai/install.sh)"
- name: Run suite against a Fire TV
env:
TVLABS_API_KEY: ${{ secrets.TVLABS_API_KEY }}
run: |
tvlabs execute ./run_suite.sh \
--target 'platform_key:fire_tv AND year>=2023' \
--build ./app.apk \
--env SUITE=smoke