#!/usr/bin/env bash set -Eeuo pipefail platform="${1:?usage: run-nightly-e2e.sh }" device_id="${2:?usage: run-nightly-e2e.sh }" if [[ "$platform" != "ios" && "$platform" != "android" ]]; then echo "Unsupported platform: $platform" >&2 exit 2 fi artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform" maestro_dir="$artifact_dir/maestro" mkdir -p "$maestro_dir" phase() { printf '%s\n' "$1" >"$artifact_dir/phase.txt" } wait_for_port() { local port="$1" local label="$2" local attempts="${3:-120}" for ((i = 1; i <= attempts; i++)); do if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then return 0 fi sleep 1 done echo "Timed out waiting for $label on port $port" >&2 return 1 } # shellcheck disable=SC2329 # Invoked through the cleanup trap call chain. stop_process_tree() { local pid="$1" local child while read -r child; do [[ -n "$child" ]] && stop_process_tree "$child" done < <(pgrep -P "$pid" 2>/dev/null || true) kill -TERM "$pid" >/dev/null 2>&1 || true } # shellcheck disable=SC2329 # Invoked by cleanup, which is registered as a trap. stop_pid_file() { local pid_file="$1" [[ -f "$pid_file" ]] || return 0 local pid pid="$(cat "$pid_file")" [[ -n "$pid" ]] || return 0 # pnpm and Expo both spawn multiple generations of children. stop_process_tree "$pid" } # shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below. cleanup() { set +e stop_pid_file "$artifact_dir/logcat.pid" stop_pid_file "$artifact_dir/metro.pid" stop_pid_file "$artifact_dir/mock-server.pid" if [[ "$platform" == "ios" ]]; then if [[ -f "$artifact_dir/redis.pid" ]]; then redis_bin="$(cat "$artifact_dir/redis-bin.txt")" "$redis_bin/redis-cli" -h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true fi if [[ -f "$artifact_dir/postgres-bin.txt" && -d "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" ]]; then postgres_bin="$(cat "$artifact_dir/postgres-bin.txt")" "$postgres_bin/pg_ctl" -D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true fi else docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \ >>"$artifact_dir/docker-services.log" 2>&1 || true docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true fi } trap cleanup EXIT INT TERM if [[ "$platform" == "android" ]]; then adb -s "$device_id" logcat -c adb -s "$device_id" logcat -v threadtime >"$artifact_dir/logcat.log" 2>&1 & printf '%s\n' "$!" >"$artifact_dir/logcat.pid" fi phase "Starting PostgreSQL, Redis, and mock server" if [[ "$platform" == "ios" ]]; then brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log" postgres_bin="$(brew --prefix postgresql@14)/bin" redis_version="7.4.7" redis_archive="${RUNNER_TEMP:-/tmp}/redis-${redis_version}.tar.gz" redis_source="${RUNNER_TEMP:-/tmp}/redis-${redis_version}" curl -fsSL -o "$redis_archive" \ "https://download.redis.io/releases/redis-${redis_version}.tar.gz" echo "c97e57b0df330a9e091cacff012bebe763c275398cf36ff44cdba876814b595b $redis_archive" \ | shasum -a 256 --check | tee -a "$artifact_dir/native-dependencies.log" rm -rf "$redis_source" tar -xzf "$redis_archive" -C "${RUNNER_TEMP:-/tmp}" make -C "$redis_source" -j "$(sysctl -n hw.ncpu)" \ 2>&1 | tee -a "$artifact_dir/native-dependencies.log" redis_bin="$redis_source/src" "$redis_bin/redis-server" --version | tee -a "$artifact_dir/native-dependencies.log" printf '%s\n' "$redis_bin" >"$artifact_dir/redis-bin.txt" postgres_data="${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" rm -rf "$postgres_data" "$postgres_bin/initdb" -D "$postgres_data" --auth=trust --username=pg --no-locale \ >"$artifact_dir/postgres-init.log" 2>&1 "$postgres_bin/pg_ctl" -D "$postgres_data" \ -o "-p 5433 -h 127.0.0.1" -l "$artifact_dir/postgres.log" start printf '%s\n' "$postgres_bin" >"$artifact_dir/postgres-bin.txt" "$redis_bin/redis-server" \ --bind 127.0.0.1 \ --port 6380 \ --save "" \ --appendonly no \ --daemonize yes \ --pidfile "$artifact_dir/redis.pid" \ --logfile "$artifact_dir/redis.log" wait_for_port 5433 "PostgreSQL" wait_for_port 6380 "Redis" pnpm --dir dev-env start:external >"$artifact_dir/mock-server.log" 2>&1 & else pnpm --dir dev-env start >"$artifact_dir/mock-server.log" 2>&1 & fi printf '%s\n' "$!" >"$artifact_dir/mock-server.pid" wait_for_port 1986 "the E2E mock-server manager" phase "Starting Metro" EXPO_PUBLIC_ENV=e2e \ NODE_ENV=development \ RN_SRC_EXT=e2e.ts,e2e.tsx \ pnpm exec expo start --dev-client --clear --port 8081 \ >"$artifact_dir/metro.log" 2>&1 & printf '%s\n' "$!" >"$artifact_dir/metro.pid" wait_for_port 8081 "Metro" # Pre-warm Metro bundle so the first Maestro flow doesn't hit a cold-start delay phase "Pre-warming Metro bundle" bundle_platform="$platform" curl -s -o /dev/null "http://localhost:8081/index.bundle?platform=${bundle_platform}&dev=true&minify=false" echo "Metro bundle pre-warmed for $bundle_platform" if [[ "$platform" == "android" ]]; then phase "Configuring Android localhost routing" adb -s "$device_id" reverse tcp:3000 tcp:3000 adb -s "$device_id" reverse tcp:8081 tcp:8081 fi phase "Running Maestro flows" set +e maestro test \ --udid "$device_id" \ --format JUNIT \ --output "$artifact_dir/report.xml" \ --config __e2e__/config.yml \ --debug-output "$maestro_dir" \ --test-output-dir "$maestro_dir" \ --flatten-debug-output \ __e2e__ \ 2>&1 | tee "$artifact_dir/maestro-cli.log" maestro_status=${PIPESTATUS[0]} set -e if [[ "$maestro_status" -eq 0 ]]; then phase "Completed" else phase "Maestro flow failure" fi exit "$maestro_status"