Scripts and CI

Run espx unattended: wait for log lines, read --json output, act on exit codes.

espx is built to run without a person watching. Every choice is a flag, nothing prompts, and the monitor can decide on its own when a run passed.

Nothing prompts

No espx command waits for input unless you pass -i. A missing choice is an error that names the flag to pass, and espflash is always told --non-interactive so its board picker never opens. -i is the opt-in for a person at a terminal:

Command-i does
espx new -iopens esp-generate's terminal UI
espx flash -ilets espflash ask which board when several are connected
espx uninstall -iasks about each item

-i without a terminal is an error (not_a_tty).

The one thing espx detects instead of being told is the serial monitor. run, monitor and flash --monitor keep espflash's key handling when stdin is a terminal, and read the port themselves when it is not. So espx run | grep panic works.

Wait for a line

espx run --until 'ready|panicked' --timeout 2m
espx run --expect 'wifi up' --expect 'ready' --reject 'PANIC|rst:0x7' --timeout 1m
espx monitor --until 'Hello world!' --timeout 30s
espx replay crash.jsonl --until 'PANIC' --fast

These flags work on run, monitor and replay. Each pattern is a regular expression in the syntax of Rust's regex-lite crate, tested against the text of each monitor line.

FlagRepeatableEffect
--until REnoExit 0 on the first line that matches
--expect REyesAn ordered script. Each regex must match a line after the line the previous one matched. Each match is an expected event with its index; the last one exits 0
--reject REyesFail at once, with exit 1, on any line that matches. Tested before the expectations on every line
--timeout DnoStop after D and exit 124. Takes 30s, 2m, 1h, or a bare number of seconds

--until is a one-pattern --expect, added after any --expects. On run the timeout clock starts when the flashed board boots, not when the build starts. When a timeout runs out, the output up to that point is still printed, and the error names the pattern still pending.

--timeout exits 124 whenever it runs out, even with no pattern to wait for. espx monitor --timeout 10s captures ten seconds of output and then exits 124. Treat 124 as "time is up", not as a crash.

If the stream ends first (a replay reaches the end of its file) with a pattern still pending, espx exits 1 with monitor_closed. With nothing pending it exits 0.

JSON output

--json changes only stdout. There are two shapes.

Commands--json prints
devices, info, chips, doctor, boards, board, deps, context, agent, logOne pretty-printed JSON document
build, flash, run, monitor, replayOne JSON event per line, each stamped with ts, host milliseconds since the epoch
devices --watchOne attached or detached event per line
{"ts":1789180727730,"event":"reset"}
{"ts":1789180727741,"event":"line","text":"ESP-ROM:esp32c6-20220919"}
{"ts":1789180727741,"event":"boot","reason":"usb","code":21,"raw":"rst:0x15 (USB_UART_HPSYS),boot:0x6c (SPI_FAST_FLASH_BOOT)"}
{"ts":1789180728120,"event":"line","text":"\u001b[32mINFO - Hello world!\u001b[0m"}
{"ts":1789180728120,"event":"expected","pattern":"Hello world!","index":0}
{"ts":1789180728120,"event":"matched","pattern":"Hello world!"}

A plain-text log line keeps esp-println's colour codes in text; a decoded defmt line carries level, module and location as separate fields. A stream ends with matched, rejected, timeout or done. build and flash end with done. Progress lines and warnings stay on stderr either way, and in --json mode every child process (cargo, espflash) has its stdout sent to stderr too, so stdout parses line by line. The JSON events reference lists every event and field, and schema/espx.json in the espx repository is the JSON Schema of all of them.

espx monitor --json --timeout 20s | jq -c 'select(.event == "panic")'
espx build --json | jq -r 'select(.event == "size") | .flash'

Errors

A failing command prints one error: line on stderr that says what to do next, and exits with a code from the table below. The line is text even with --json. The stable, machine-readable error codes (build_failed, board_not_attached, policy_denied) appear in MCP tool results and in the agent audit log; see Exit codes.

Exit codes

CodeMeaning
0Success, including a --until match or the last --expect match
1Any other failure: a build error, a rejected line, a missing board, a bad flag
124--timeout ran out (the coreutils convention)
130A child process was interrupted, usually your Ctrl+C reaching cargo or espflash. Nothing is printed

In CI

A CI job with a board attached to the runner can build, flash and check the firmware in a few lines:

test-on-board.sh
#!/bin/sh
set -e
export ESPX_NO_UPDATE_CHECK=1

espx setup
espx build --json > build.jsonl
espx run --expect 'boot ok' --expect 'self-test passed' \
         --reject 'PANIC|panicked|rst:0x7' \
         --timeout 2m --json > run.jsonl
  • espx setup is idempotent, so it costs nothing on a runner that already has the tools.
  • ESPX_NO_UPDATE_CHECK (any value) stops the daily background update check.
  • Bind the project to the runner's board with [board] id in espx.toml, or pass --port, when the runner has more than one board attached.
  • Add --record run.jsonl to espx monitor to keep a session as a build artifact; espx replay plays it back anywhere, without the board. See Monitor and replay.

Builds and replays need no board, so the parts of a pipeline that check recorded output can run on any machine.