- Swift 95.7%
- Python 4.3%
e2e
"Automation Running"? Not on my machine!
e2e runs your existing test/build commands inside a macOS VM over SSH + rsync, so UI automation stays isolated from your host desktop.
Why use this
- Keep host mouse/keyboard free while XCUITests run in the guest.
- Reuse your existing project commands (
make,xcodebuild, scripts). - Get deterministic orchestration: sync -> run -> pull artifacts.
- Capture machine-readable JSONL traces for debugging and regression tests.
Quick start
e2e init # scaffold .e2e.env and e2e/excludes.rsync
nano .e2e.env # fill in SSH host, paths, etc.
e2e doctor # verify host, guest, and connectivity
e2e config # inspect resolved configuration
e2e exec -- make smoke # sync, run, pull artifacts
Commands
e2e init
e2e init
Creates starter config files without overwriting existing ones:
.e2e.env— template with all required and optional variables.e2e/excludes.rsync— default rsync exclude patterns (.git/,.build/,.e2e.env, etc.).
Idempotent: existing files are preserved.
e2e doctor
e2e doctor [--env-file <path>] [--timeout-ssh <seconds>]
Runs a checklist that validates your setup end-to-end:
| # | Check | What it does |
|---|---|---|
| 1 | config loaded | parses config and checks required keys |
| 2 | host tools present | looks for ssh, rsync, xcodebuild on PATH |
| 3 | host rsync --mkpath |
runs rsync --mkpath --version to verify support |
| 4 | SSH connectivity | ssh <opts> user@host "echo ok" |
| 5 | guest tools reachable | checks rsync, xcodebuild, mise over SSH |
| 6 | host xcodebuild readable | runs xcodebuild -version locally |
| 7 | host/guest Xcode parity | compares xcodebuild -version output; fails on mismatch |
Output uses [x] / [ ] prefixes with hints on failure:
[x] config loaded
[x] host tool present: ssh
[x] host tool present: rsync
[ ] host tool present: xcodebuild
hint: install xcodebuild and ensure it is on PATH before running e2e doctor
Exits 0 when all checks pass, 1 if any check fails.
e2e config
e2e config [--env-file <path>] [<key>]
Prints the resolved configuration after merging dotenv, environment variables, and defaults.
- With no arguments, prints all
KEY=valuepairs (one per line). - With a key name, prints just the value for that key.
Output goes to stdout so it's pipeable:
e2e config # all keys
e2e config E2E_SSH_HOST # just the value
HOST=$(e2e config E2E_SSH_HOST) # capture in a variable
e2e config --env-file .e2e.local # use alternate env file
e2e config set
e2e config set [--env-file <path>] [KEY VALUE]
Updates a configuration key in the dotenv file.
- With
KEY VALUE, writes the specified value non-interactively. - With no arguments, walks through every configuration key in order, showing the current value as the default. Press Enter to keep it, or type a new value. Only keys whose values change are written back to the file.
Validates types: integers for port/timeout keys, booleans for toggle keys.
e2e config set E2E_SSH_HOST vm.local # set a single key
e2e config set E2E_SSH_PORT 2222 # validated as integer
e2e config set --env-file .e2e.local E2E_SSH_PORT 2222
e2e config set # interactive walkthrough
e2e exec
e2e exec [options] -- <command...>
The main command. Syncs your project to the guest, runs a command there, and pulls artifacts back.
Options:
| Flag | Default | Description |
|---|---|---|
--no-clear-out |
clears | skip clearing E2E_GUEST_OUT before execution |
--verbose |
off | print the SSH command being executed |
--env-file <path> |
.e2e.env |
path to dotenv config file |
--timeout-ssh <seconds> |
5 |
SSH connection timeout |
Pipeline phases:
- Preflight — SSH connectivity check (
ssh ... "echo ok"). - Sync host → guest —
rsyncthe project fromE2E_HOST_ROOTtoE2E_GUEST_ROOT. - Clear guest output —
rm -rf+mkdir -ponE2E_GUEST_OUT(unless--no-clear-out). - Guest exec — runs your command via
ssh ... "cd <guest_root> && <command>". - Pull guest → host —
rsyncartifacts fromE2E_GUEST_OUTtoE2E_HOST_OUT.
Pull always runs even if the guest command fails, so you still get test artifacts. If the guest command succeeded but pull fails, the pull exit code is returned. If the guest command failed, its exit code is returned regardless of pull outcome.
Status lines go to stderr:
[e2e] preflight completed in 0.42s
[e2e] sync host->guest completed in 2.34s
[e2e] guest_exec failed in 18.91s
[e2e] pull guest->host completed in 0.87s
Examples:
e2e exec -- make smoke
e2e exec --no-clear-out --verbose -- make xctest
e2e exec --env-file .e2e.local --timeout-ssh 10 -- make smoke
e2e provision
e2e provision --script <path> [--env-file <path>] [--timeout-ssh <seconds>]
Runs a provisioning script on the guest. The script must be inside E2E_HOST_ROOT so it gets synced over before execution.
Under the hood this is e2e exec with:
- the command set to
bash <relative-script-path> - guest output clearing disabled
e2e provision --script setup/bootstrap.sh
Exit codes
| Code | Meaning |
|---|---|
0 |
success |
1 |
runtime or config error |
2 |
usage / argument error |
For exec, the guest command's exit code is forwarded directly. For config set, exit code 2 indicates an unknown key and exit code 1 indicates an invalid value or a write failure.
Configuration
Load order (highest priority first):
- CLI flags
- Process environment
.e2e.env(or--env-filepath)
Required keys:
| Key | Description |
|---|---|
E2E_SSH_HOST |
hostname or IP of guest VM |
E2E_SSH_USER |
SSH username |
E2E_HOST_ROOT |
local project directory to sync |
E2E_GUEST_ROOT |
guest-side project directory |
E2E_GUEST_OUT |
guest-side output/artifact directory |
E2E_HOST_OUT |
local directory to pull artifacts into |
Optional keys:
| Key | Default | Description |
|---|---|---|
E2E_SSH_PORT |
22 |
SSH port |
E2E_SSH_OPTS |
-o BatchMode=yes -o StrictHostKeyChecking=accept-new |
extra SSH flags |
E2E_RSYNC_OPTS |
-az --delete --mkpath |
rsync flags |
E2E_RSYNC_EXCLUDES |
— | comma-separated exclude patterns |
E2E_RSYNC_EXCLUDE_FILE |
e2e/excludes.rsync if present |
path for --exclude-from |
E2E_CLEAR_GUEST_OUT |
1 |
clear guest output before exec (0/1) |
E2E_TIMEOUT_SSH |
5 |
SSH timeout in seconds |
E2E_PRINT_COMMAND |
0 |
print SSH commands being executed (0/1) |
E2E_TRACE_FILE |
— | path to JSONL trace file |
JSONL tracing
Set E2E_TRACE_FILE to capture structured events:
E2E_TRACE_FILE=.e2e-trace.jsonl e2e exec -- make smoke
Each line is a JSON object with this shape:
kind:phase_start | phase_end | command_start | command_exit | command_error | process_exitphase: optional phase name (preflight,sync_host_to_guest,clear_guest_out,guest_exec,pull_guest_to_host)argv: optional command argv arrayexitCode: optional integerdurationMs: optional integer duration in milliseconds (present forphase_endandprocess_exit)timestamp: ISO-8601 timestampnote: optional string
Example lines:
{"kind":"phase_start","phase":"preflight","argv":null,"exitCode":null,"timestamp":"2026年02月24日T23:24:00Z","note":null}
{"kind":"command_start","phase":"preflight","argv":["ssh","-p","22","runner@vm.local","echo ok"],"exitCode":null,"timestamp":"2026年02月24日T23:24:00Z","note":null}
{"kind":"command_exit","phase":"preflight","argv":["ssh","-p","22","runner@vm.local","echo ok"],"exitCode":0,"timestamp":"2026年02月24日T23:24:00Z","note":null}
{"kind":"process_exit","phase":null,"argv":null,"exitCode":0,"timestamp":"2026年02月24日T23:24:01Z","note":null}