# Running flows in CI

Once a flow's YAML exists, running it needs **no AI and no MCP** — just Node, `adb`, and a device. `ai-mobile-tester run` is built for that: it talks to whatever device `adb` sees and speaks the universal CI contract — an **exit code**, **on-disk artifacts**, and **environment variables** — so it drops into any CI (GitHub Actions, a MacStadium-hosted Jenkins/Buildkite, GitLab, …) on macOS or Linux.

## The runner

```bash
ai-mobile-tester run <flow.yaml> [options]
```

| Option               | Meaning                                                                                             |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `--device <serial>`  | Target a device. Default: the only attached one (errors if none, or asks you to choose if several). |
| `--env KEY=VAL`      | Set/override a flow variable (repeatable).                                                          |
| `--output-dir <dir>` | Where to write run artifacts (default `.mobile-runs/<timestamp>/`).                                 |
| `--junit <path>`     | Also write a JUnit XML report.                                                                      |
| `--no-heal`          | Disable self-heal (strict replay).                                                                  |

**Exit codes:** `0` all steps passed · `1` ran but a step failed · `2` could not run (validation error, no/ambiguous device, bad flags).

`ai-mobile-tester validate <flow.yaml>` statically validates a flow (no device) — a fast pre-check; exit `0`/`2`.

## Secrets

Reference secrets as `${VAR}` and **declare them in the flow's top-level `env:` block** (a placeholder or empty value is fine — keep real secrets out of the committed YAML):

```yaml
appId: com.example.app
env:
  TEST_USER: ""
  TEST_PASS: ""
---
- inputText: { into: { id: email }, text: "${TEST_USER}" }
```

At run time each declared variable resolves with precedence `--env KEY=VAL` > the process environment > the flow default. So in CI you inject the secret as an environment variable and it flows in automatically — nothing in the YAML changes.

**Naming tip:** avoid flow variable names that collide with standard OS environment variables (`USER`, `HOME`, `PATH`, `SHELL`, `LANG`, `PWD`); because `process.env` takes precedence over a flow default, a reader running locally would get their OS value silently substituted for the intended placeholder.

## Artifacts

Every run writes `report.html` (human-readable) into the output dir; `--junit` adds a machine-readable XML your CI can show per-step. Collect both as build artifacts. Note: `--junit` is written only when the flow actually runs; a could-not-run failure (exit 2) produces no JUnit file — the non-zero exit still fails the build.

## A generic recipe

```bash
# 1. ensure a device is visible (emulator or attached phone)
adb wait-for-device
# 2. run; the exit code fails the build on a step failure
ai-mobile-tester run flows/login.yaml --junit results.xml --output-dir artifacts
# (CI then collects artifacts/report.html and results.xml, and surfaces results.xml in its test UI)
```

## Illustrative GitHub Actions job (emulator)

```yaml
jobs:
  ui-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install -g ai-mobile-tester
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          script: ai-mobile-tester run flows/login.yaml --junit results.xml
        env:
          TEST_USER: ${{ secrets.TEST_USER }}
          TEST_PASS: ${{ secrets.TEST_PASS }}
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: ui-test-report, path: "**/report.html" }
```

For a real device or a hybrid/WebView app that needs one (e.g. a VPN or specific hardware), run on a self-hosted runner or a device farm with `adb` access — the command is identical; only the device source changes.
