|
| 1 | +# Local iOS E2E reproduction (Tart) |
| 2 | + |
| 3 | +Self-contained Tart VM pipeline for reproducing **Testing E2E iOS** locally. Full design: [okf-bundle/testing/local-ios-e2e-reproduction.md](../../okf-bundle/testing/local-ios-e2e-reproduction.md). |
| 4 | + |
| 5 | +**Isolation:** everything lives under `scripts/tart/` plus OKF documentation. This system does not modify `.github/`, CI workflows, or package scripts. |
| 6 | + |
| 7 | +## Prerequisites (host) |
| 8 | + |
| 9 | +- Apple Silicon Mac with [Tart](https://tart.run/) installed |
| 10 | +- `sshpass` (`brew install sshpass`) |
| 11 | +- **≥ 80 GiB free** on the volume holding `~/.tart` (symlink to external disk if needed) |
| 12 | +- Git worktree for the branch under test (not the primary clone) |
| 13 | + |
| 14 | +## Quick start |
| 15 | + |
| 16 | +```bash |
| 17 | +# 1. Bake golden toolchain (once, after disk cleanup) |
| 18 | +./scripts/tart/bake-golden.sh |
| 19 | + |
| 20 | +# 2. Prewarm caches on origin/main (once per main bump) |
| 21 | +./scripts/tart/bake-warmed.sh /path/to/any/rnfb-clone-or-worktree |
| 22 | + |
| 23 | +# 3. Run one ephemeral iteration on your worktree |
| 24 | +./scripts/tart/run-ephemeral.sh --worktree /path/to/worktree --mode debug |
| 25 | + |
| 26 | +# 4. Stress loop |
| 27 | +./scripts/tart/run-stress.sh --worktree /path/to/worktree --count 10 --mode debug |
| 28 | +``` |
| 29 | + |
| 30 | +## Yarn cache |
| 31 | + |
| 32 | +All tart scripts export: |
| 33 | + |
| 34 | +```bash |
| 35 | +YARN_CACHE_FOLDER=$HOME/.yarn-rnfb-cache |
| 36 | +``` |
| 37 | + |
| 38 | +The repo uses `enableGlobalCache: false` in `.yarnrc.yml`; the env var keeps Berry cache outside the virtiofs worktree without changing repo config. |
| 39 | + |
| 40 | +## Scripts |
| 41 | + |
| 42 | +| Script | Where | |
| 43 | +|--------|-------| |
| 44 | +| `bake-golden.sh` | Host — seed → golden VM | |
| 45 | +| `bake-warmed.sh` | Host — golden → warmed VM | |
| 46 | +| `run-ephemeral.sh` | Host — one iteration, delete VM | |
| 47 | +| `run-stress.sh` | Host — N iterations | |
| 48 | +| `stop-ephemeral.sh` | Host — stop/delete a kept ephemeral VM | |
| 49 | +| `provision-toolchain.sh` | In-VM (via SSH during bake) | |
| 50 | +| `prewarm-caches.sh` | In-VM (via SSH during warm bake) | |
| 51 | +| `run-iteration.sh` | In-VM (via SSH during ephemeral run) | |
| 52 | +| `verify-manifest.sh` | Host | |
| 53 | +| `report-disk-usage.sh` | Host | |
| 54 | + |
| 55 | +CI helper equivalents are vendored under `lib/` and `vendor/homebrew-rnfb/` — not imported from `.github/`. |
| 56 | + |
| 57 | +## Completion, artifacts, and cleanup |
| 58 | + |
| 59 | +`run-ephemeral.sh` launches the iteration **detached inside the VM** (`nohup`), then polls virtiofs for `iteration-complete.json`. No long-lived SSH log stream. |
| 60 | + |
| 61 | +### How completion is detected |
| 62 | + |
| 63 | +1. Host SSHs once to start `run-iteration.sh` under `nohup` on VM disk (`~/rnfb-prewarm/...`). |
| 64 | +2. In-VM `run-iteration.sh` logs to `run-iteration.log` on VM disk (not over SSH). |
| 65 | +3. When finished (or on failure via `EXIT` trap), it writes **`iteration-complete.json`** and syncs small files to virtiofs. |
| 66 | +4. Host polls the worktree for `iteration-complete.json`, reads `rc`, then **SCPs bulk logs** from the VM (while it is still running, unless `--no-sync-artifacts`). |
| 67 | +5. Orchestrator tears down the VM on exit (or leaves it running with `--keep-vm`). |
| 68 | + |
| 69 | +**Artifact flow (two paths):** |
| 70 | + |
| 71 | +| Stage | What | Where | |
| 72 | +|-------|------|--------| |
| 73 | +| In-VM write | Full iteration output during the run | `~/rnfb-prewarm/scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/` on VM disk | |
| 74 | +| Virtiofs sync | Small completion files (`iteration-complete.json`, `detox-step.log`, `ccache-stats.txt`, …) | Same path under the mounted worktree (host-visible without SSH) | |
| 75 | +| Host SCP harvest (default) | Bulk files (`run-iteration.log`, `simulator.log`, `simulator.mp4`, …) | Host pulls from VM → worktree after completion marker | |
| 76 | + |
| 77 | +Use **`--no-sync-artifacts`** to skip the SCP harvest step. Essentials still land on the worktree via virtiofs; bulk logs remain on the VM until teardown (or inspect with `--keep-vm` + SSH). |
| 78 | + |
| 79 | +The VM **stays running** from `tart run` through iteration and SCP harvest. Nothing stops it until the host orchestrator calls teardown at exit (or you pass `--keep-vm` to leave it up). |
| 80 | + |
| 81 | +Live status (optional): |
| 82 | + |
| 83 | +```bash |
| 84 | +ssh admin@$(tart ip rnfb-e2e-<uuid>) tail -f ~/rnfb-prewarm/scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/run-iteration.log |
| 85 | +``` |
| 86 | + |
| 87 | +`bake-warmed.sh` uses a similar marker pattern: in-VM `prewarm-caches.sh` writes **`prewarm-complete.json`**; the host polls for it. |
| 88 | + |
| 89 | +### Artifact layout |
| 90 | + |
| 91 | +All run outputs live under **`scripts/tart/artifacts/<SESSION_ID>/`**, where `SESSION_ID` is the UTC run start timestamp (e.g. `20260623T062250Z`). The directory is gitignored. |
| 92 | + |
| 93 | +| Path | Purpose | |
| 94 | +|------|---------| |
| 95 | +| `<SESSION_ID>/run-ephemeral.log` | Host orchestrator log (APFS, not virtiofs) | |
| 96 | +| `<SESSION_ID>/<mode>-iter<N>/` | Per-iteration artifacts (see below) | |
| 97 | +| `<SESSION_ID>/prewarm.log` | `bake-warmed.sh` prewarm log | |
| 98 | + |
| 99 | +### Artifact files (per iteration) |
| 100 | + |
| 101 | +Iteration dir: `scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/` (e.g. `debug-iter0`). |
| 102 | + |
| 103 | +| File | Purpose | |
| 104 | +|------|---------| |
| 105 | +| `iteration-complete.json` | Authoritative completion marker + exit code | |
| 106 | +| `run-iteration.log` | Full in-VM iteration log (SCP'd on harvest) | |
| 107 | +| `iteration-env.txt` | Lockfile SHAs, warmed-manifest comparison, build duration | |
| 108 | +| `ccache-stats.txt` | `ccache -s` snapshots at each phase | |
| 109 | +| `detox-step.log` | Detox/Jet test output | |
| 110 | +| `flake-summary.txt` | Post-run flake digest | |
| 111 | + |
| 112 | +Host wrapper log: `scripts/tart/artifacts/<SESSION_ID>/run-ephemeral.log` (host APFS, not virtiofs). |
| 113 | + |
| 114 | +## Operational pitfalls (learned in practice) |
| 115 | + |
| 116 | +### Do not stream iteration output over SSH or virtiofs |
| 117 | + |
| 118 | +Build output (xcbeautify note spam) can block `tee` on virtiofs and wedge the whole pipeline. Iteration stdout belongs on **VM disk**; the host waits on **`iteration-complete.json`** only. |
| 119 | + |
| 120 | +Bulk artifacts (`simulator.log`, `testing.log`, `simulator.mp4`, `run-iteration.log`) are pulled with **host-side SCP** after the completion marker appears. |
| 121 | + |
| 122 | +### Do not rsync large logs over virtiofs |
| 123 | + |
| 124 | +The iteration exit path **must not** block on copying hundreds of MB through virtiofs. Essential files only (`iteration-complete.json`, `detox-step.log`, etc.). |
| 125 | + |
| 126 | +### SSH sessions can outlive the VM |
| 127 | + |
| 128 | +`rnfb_tart_ssh` connects to `tart ip <vm>`. Problems observed on older flows: |
| 129 | + |
| 130 | +- Long-lived SSH streams blocking cleanup (mitigated by detached iteration launch). |
| 131 | +- **Deleted VMs leave zombie `sshpass` processes** still connected to stale `192.168.64.*` addresses. |
| 132 | + |
| 133 | +Mitigations in `lib/common.sh`: |
| 134 | + |
| 135 | +- `ServerAliveInterval=15` / `ServerAliveCountMax=4` — dead connections drop in ~60s |
| 136 | +- `timeout` wrapper on SSH/SCP (default **`RNFB_TART_SSH_TIMEOUT=3600`** seconds for launch/harvest) |
| 137 | +- **`RNFB_TART_ITERATION_TIMEOUT=7200`** — max wait for `iteration-complete.json` on virtiofs |
| 138 | + |
| 139 | +### Low CPU does not mean the pipeline finished |
| 140 | + |
| 141 | +A completed test run can still leave: |
| 142 | + |
| 143 | +- An ephemeral VM running (host script blocked on SSH) |
| 144 | +- Zombie `sshpass` / `run-ephemeral.sh` / `bake-warmed.sh` wrappers on the host |
| 145 | +- `tart run` background processes |
| 146 | + |
| 147 | +Always check process state explicitly. |
| 148 | + |
| 149 | +## Troubleshooting |
| 150 | + |
| 151 | +### Check for zombie host processes |
| 152 | + |
| 153 | +```bash |
| 154 | +pgrep -fl 'sshpass|run-ephemeral|bake-warmed|tart run' |
| 155 | +tart list |
| 156 | +``` |
| 157 | + |
| 158 | +- Any **`sshpass` to `192.168.64.*`** with no matching **`rnfb-e2e-*`** VM in `tart list` → safe to kill. |
| 159 | +- Multiple overlapping `run-ephemeral.sh` from retries → kill extras; only one should own the ephemeral VM. |
| 160 | + |
| 161 | +```bash |
| 162 | +pkill -f 'sshpass.*192.168.64' |
| 163 | +pkill -f 'scripts/tart/run-ephemeral.sh' |
| 164 | +``` |
| 165 | + |
| 166 | +### Check iteration outcome |
| 167 | + |
| 168 | +```bash |
| 169 | +ls scripts/tart/artifacts/*/ |
| 170 | +cat scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/iteration-complete.json |
| 171 | +cat scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/ccache-stats.txt |
| 172 | +tail scripts/tart/artifacts/<SESSION_ID>/<mode>-iter<N>/detox-step.log |
| 173 | +``` |
| 174 | + |
| 175 | +### Manual VM cleanup |
| 176 | + |
| 177 | +Ephemeral VMs are named `rnfb-e2e-<uuid>`. By default `run-ephemeral.sh` stops and deletes them on exit. With `--keep-vm`, the orchestrator leaves the VM running: |
| 178 | + |
| 179 | +```bash |
| 180 | +./scripts/tart/stop-ephemeral.sh rnfb-e2e-<uuid> # stop only |
| 181 | +./scripts/tart/stop-ephemeral.sh rnfb-e2e-<uuid> --delete # stop + delete image |
| 182 | +``` |
| 183 | + |
| 184 | +Or manually: |
| 185 | + |
| 186 | +```bash |
| 187 | +tart list |
| 188 | +tart stop rnfb-e2e-<uuid> |
| 189 | +tart delete rnfb-e2e-<uuid> |
| 190 | +``` |
| 191 | + |
| 192 | +Preserve **`rnfb-ios-e2e-golden`** and **`rnfb-ios-e2e-warmed`** (stopped is normal). |
| 193 | + |
| 194 | +### `bake-warmed` appeared finished but host hung |
| 195 | + |
| 196 | +If in-VM prewarm logged `[prewarm] complete` but the host never reached `[bake-warmed] tagging`: |
| 197 | + |
| 198 | +1. Check for stuck `sshpass` / `bake-warmed.sh`. |
| 199 | +2. Confirm `~/rnfb-tart-manifests/prewarm-complete.json` exists in the build VM (or fetch `warmed-main.json` manually). |
| 200 | +3. Tag manually: `tart clone rnfb-ios-e2e-warmed-build rnfb-ios-e2e-warmed`, then delete the `-build` VM. |
| 201 | + |
| 202 | +### When to rebake warmed |
| 203 | + |
| 204 | +Re-run `bake-warmed.sh` when `origin/main` changes **`yarn.lock`** or **`tests/ios/Podfile.lock`** (compare SHAs in `manifests/warmed-main.json`). Golden rebake only when CI toolchain pins change (`manifests/golden-expected.json`). |
| 205 | + |
| 206 | +## Environment variables (SSH / caches) |
| 207 | + |
| 208 | +| Variable | Default | Notes | |
| 209 | +|----------|---------|-------| |
| 210 | +| `RNFB_TART_SSH_TIMEOUT` | `3600` | Max seconds per SSH/SCP call (`timeout` wrapper) | |
| 211 | +| `RNFB_TART_ITERATION_TIMEOUT` | `7200` | Max seconds to poll for `iteration-complete.json` | |
| 212 | +| `RNFB_TART_SSH_USER` / `RNFB_TART_SSH_PASSWORD` | `admin` / `admin` | Cirrus Tart images | |
| 213 | +| `YARN_CACHE_FOLDER` | `~/.yarn-rnfb-cache` | Outside virtiofs worktree | |
| 214 | +| `CCACHE_DIR` | `~/.ccache` | Shared between prewarm and iteration via `~/rnfb-prewarm` path | |
| 215 | +| `RNFB_TART_PREWARM_DIR` | `~/rnfb-prewarm` | Iteration materializes worktree here for ccache key alignment | |
| 216 | + |
0 commit comments