Skip to main content

tvlabs execute

Experimental

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:

VariableDescription
TVLABS_SESSION_IDThe session's UUID
TVLABS_SESSION_URLLink to the session in the TV Labs web interface
TVLABS_PLATFORMPlatform key of the matched device, e.g. fire_tv
TVLABS_DEVICE_HOSTHost the tunnel is bound to (see --bind-address)
TVLABS_INSPECTOR_PORTLocal port forwarded to the device's web inspector
TVLABS_APP_PORTPort 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:

CodeMeaning
entrypoint's ownYour harness ran to completion. Its status is the command's status
1A local error — bad flags, or a --target the server rejects
124No device was provisioned and connected within --match-timeout
125The platform failed before your entrypoint started. Safe to retry
126The session died under a running entrypoint. Any results it published are partial
130 / 143Interrupted 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

FlagDefaultDescription
--targetrequiredKQL device query
--entrypointScript or binary to run. The positional argument is the shorter spelling
--context.Working directory for the entrypoint
--envKEY=VALUE passed to the entrypoint. Repeatable
--buildPath to a build file to upload and install during warmup
--build-idExisting build id to install during warmup
--match-timeout30mHow long to wait for a device. Does not bound your entrypoint's run
--inspector-port, -i9222Local port forwarded to the device's web inspector
--app-port, -p50552Port of a locally running web application to forward to the device
--bind-address, -blocalhostAddress to bind the tunnel to, and the TVLABS_DEVICE_HOST your entrypoint dials
note

--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