mattermost-mobile-upstream/detox/scripts/preboot_ios_simulator.sh
yasser khan 587e15ea89
chore(e2e): Cache app builds. Skip E2E on docs only PR. (#9930)
Based on the later commits (after the earlier experimentation and reversions), the meaningful end state of the PR is:

**Summary:** Improve E2E CI reliability, performance, and security by introducing robust build caching with safe fingerprinting and cache isolation, duration-based test sharding with graceful fallbacks, more reliable E2E impact detection and status handling, stronger cancellation/concurrency behavior, multiple iOS Detox stability fixes, and hardening GitHub Actions to avoid executing PR-authored code. Also includes CI fixes for iOS builds and a temporary Android E2E test skip for a known regression.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-08-04 19:13:39 +05:30

251 lines
9.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Pre-boot an iOS simulator for Detox/Maestro CI.
#
# Optimized vs the former inline Detox workflow script:
# - One blocking bootstatus for sims that already have CoreSimulator dirs (typical CI).
# - Autofill plists written while shutdown — no "init boot just to mkdir" on warm sims.
# - Skips autofill re-configuration when already applied (marker + plist check).
# - Drops the redundant 8–10s bootstatus poll after bootstatus already completed.
# - Brand-new simulators (simctl create) still use boot → shutdown → configure → boot.
#
# Requires: DEVICE_NAME, DEVICE_OS_VERSION. Writes SIMULATOR_ID to GITHUB_ENV when set.
# Optional:
# PREBOOT_SKIP_PREWARM=1 — Maestro only (uses listapps readiness). Detox must pre-warm.
# PREBOOT_PREWARM_SECS — first pre-warm wait (default 15; iPad often needs 10–15s).
#
# Grants notifications only. Maestro's Calls flows need mic/camera and grant them
# themselves per batch (detox/maestro/scripts/run_ci_batches.sh
# grant_ios_calls_permissions), so this script does not — see grant_notifications
# for why extra privacy grants are avoided here.
set -euo pipefail
readonly BUNDLE_ID="com.mattermost.rnbeta"
readonly AUTOFILL_MARKER="mattermost-ci-autofill-v1"
readonly PREWARM_SECS="${PREBOOT_PREWARM_SECS:-15}"
readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
log() {
echo "[preboot $(date +%H:%M:%S)] $*"
}
sim_state() {
xcrun simctl list devices 2>/dev/null | grep "$SIMULATOR_ID" | sed -E 's/.*\((Booted|Shutdown|Booting|Creating)\)$/\1/' || echo "Unknown"
}
shutdown_if_booted() {
if [ "$(sim_state)" = "Booted" ]; then
log "Shutting down simulator to edit autofill plists..."
xcrun simctl shutdown "$SIMULATOR_ID" || true
for _ in 1 2 3 4 5 6 7 8; do
[ "$(sim_state)" = "Shutdown" ] && return 0
sleep 0.5
done
log "Warning: simulator may not have reached Shutdown state"
fi
}
boot_and_wait() {
log "Booting simulator $SIMULATOR_ID..."
xcrun simctl boot "$SIMULATOR_ID" 2>/dev/null || true
log "Waiting for boot to complete (blocking bootstatus)..."
xcrun simctl bootstatus "$SIMULATOR_ID"
}
autofill_already_configured() {
[ -f "$AUTOFILL_STAMP" ] && return 0
[ -f "$SETTINGS_PLIST" ] || return 1
plutil -extract restrictedBool.allowPasswordAutoFill.value raw "$SETTINGS_PLIST" 2>/dev/null | grep -qi 'false'
}
configure_autofill_offline() {
log "Disabling password autofill (simulator shut down)..."
mkdir -p "$SETTINGS_DIR"
if ! (cd "$REPO_ROOT/detox" && node utils/disable_ios_autofill.js --simulator-id "$SIMULATOR_ID"); then
echo "Failed to disable password autofill"
exit 1
fi
touch "$AUTOFILL_STAMP"
}
seed_password_defaults() {
log "Seeding Passwords.app defaults (best-effort)..."
xcrun simctl spawn "$SIMULATOR_ID" defaults write com.apple.Passwords AutoFill -bool NO 2>/dev/null || true
xcrun simctl spawn "$SIMULATOR_ID" defaults write com.apple.Passwords AutoSave -bool NO 2>/dev/null || true
xcrun simctl spawn "$SIMULATOR_ID" defaults write com.apple.Passwords credentialSaveNotificationsEnabled -bool NO 2>/dev/null || true
xcrun simctl spawn "$SIMULATOR_ID" defaults write com.apple.springboard AutoFillPasswords -bool NO 2>/dev/null || true
}
install_app() {
local app_path
app_path=$(ls -d "$REPO_ROOT"/mobile-artifacts/*.app 2>/dev/null | head -1)
if [ -z "$app_path" ]; then
echo "No .app bundle found in mobile-artifacts/"
ls -la "$REPO_ROOT/mobile-artifacts/" || true
exit 1
fi
log "Installing $app_path..."
xcrun simctl install "$SIMULATOR_ID" "$app_path"
}
grant_notifications() {
# Match proven Detox CI: notifications only. Deny camera/photos corrupts TCC on iOS 26.x;
# keep grants minimal so a failed privacy call does not cascade into broken UI hit-testing.
log "Pre-granting notification permission..."
if ! xcrun simctl privacy "$SIMULATOR_ID" grant notifications "$BUNDLE_ID"; then
log "Warning: notification grant failed (continuing; Detox may re-request at launch)"
fi
}
kill_app_via_launchd() {
local app_pid
app_pid=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list 2>/dev/null | \
grep "$BUNDLE_ID" | awk '{print $1}' | grep -E '^[0-9]+$' || true)
if [ -z "$app_pid" ]; then
return 0
fi
xcrun simctl spawn "$SIMULATOR_ID" kill -9 "$app_pid" 2>/dev/null || true
# launchd teardown is asynchronous — poll until the bundle leaves the job list
# so a timeout is not reported as a successful kill.
for _ in $(seq 12); do
if ! xcrun simctl spawn "$SIMULATOR_ID" launchctl list 2>/dev/null | grep -q "$BUNDLE_ID"; then
log "Killed app via launchd (PID $app_pid)"
return 0
fi
sleep 0.25
done
log "Warning: app still listed in launchd 3s after kill -9 (PID $app_pid)"
return 1
}
prewarm_app() {
local sleep_time="${1:-$PREWARM_SECS}"
log "Pre-warming app (${sleep_time}s launch window)..."
xcrun simctl launch "$SIMULATOR_ID" "$BUNDLE_ID" 2>/dev/null &
local launch_pid=$!
sleep "$sleep_time"
kill "$launch_pid" 2>/dev/null || true
wait "$launch_pid" 2>/dev/null || true
if ! kill_app_via_launchd; then
log "Pre-warm cleanup did not complete — treating pre-warm as failed"
return 1
fi
if xcrun simctl get_app_container "$SIMULATOR_ID" "$BUNDLE_ID" data 2>/dev/null; then
log "Data container verified"
return 0
fi
log "Data container not found after pre-warm"
return 1
}
wait_ready_quick() {
# bootstatus already blocked until ready; one -b check avoids an extra fixed sleep.
if xcrun simctl bootstatus "$SIMULATOR_ID" -b 2>/dev/null; then
log "Simulator ready"
return 0
fi
log "Quick readiness check failed — waiting up to 5s..."
for _ in 1 2 3 4 5; do
sleep 1
xcrun simctl bootstatus "$SIMULATOR_ID" -b 2>/dev/null && return 0
done
log "Warning: simulator may not be fully ready"
return 0
}
verify_health() {
if ! xcrun simctl bootstatus "$SIMULATOR_ID" -b 2>/dev/null; then
log "Simulator unresponsive — rebooting..."
xcrun simctl shutdown "$SIMULATOR_ID" || true
sleep 2
boot_and_wait
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID" || true
fi
if ! xcrun simctl get_app_container "$SIMULATOR_ID" "$BUNDLE_ID" 2>/dev/null; then
log "App missing — reinstalling..."
install_app
fi
sudo mdutil -a -i off 2>/dev/null || true
}
find_or_create_simulator() {
local device_name="${DEVICE_NAME:?DEVICE_NAME is required}"
local os_version="${DEVICE_OS_VERSION:?DEVICE_OS_VERSION is required}"
log "Looking for simulator: $device_name ($os_version)"
SIMULATOR_ID=$(xcrun simctl list devices | grep "$device_name" | grep "$os_version" | head -1 | grep -oE '([0-9A-F-]{36})' || true)
if [ -z "$SIMULATOR_ID" ]; then
SIMULATOR_ID=$(xcrun simctl list devices "$os_version" 2>/dev/null | grep "$device_name" | head -1 | grep -oE '([0-9A-F-]{36})' || true)
fi
if [ -n "$SIMULATOR_ID" ]; then
log "Found existing simulator: $SIMULATOR_ID"
return 0
fi
log "Creating simulator..."
local device_type runtime
device_type=$(xcrun simctl list devicetypes | grep "${device_name} (" | head -1 | awk -F'[()]' '{print $(NF-1)}')
runtime=$(xcrun simctl list runtimes | grep "$os_version" | head -1 | sed 's/.* - \(.*\)/\1/')
if [ -z "$device_type" ] || [ -z "$runtime" ]; then
echo "Could not resolve device type or runtime for $device_name / $os_version"
exit 1
fi
SIMULATOR_ID=$(xcrun simctl create "CI-${device_name}" "$device_type" "$runtime")
log "Created simulator: $SIMULATOR_ID"
export SIMULATOR_NEEDS_INIT_BOOT=1
}
# ─── Main ────────────────────────────────────────────────────────────────────
find_or_create_simulator
SETTINGS_DIR="$HOME/Library/Developer/CoreSimulator/Devices/$SIMULATOR_ID/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles"
SETTINGS_PLIST="$SETTINGS_DIR/UserSettings.plist"
AUTOFILL_STAMP="$SETTINGS_DIR/.$AUTOFILL_MARKER"
if autofill_already_configured; then
log "Autofill restrictions already configured — skipping plist edit"
if [ "$(sim_state)" != "Booted" ]; then
boot_and_wait
else
log "Simulator already booted"
fi
elif [ -n "${SIMULATOR_NEEDS_INIT_BOOT:-}" ] || [ ! -d "$SETTINGS_DIR" ]; then
# Fresh simulator: one boot creates system group dirs, then configure offline, then boot again.
log "New simulator — init boot to create CoreSimulator dirs..."
boot_and_wait
shutdown_if_booted
configure_autofill_offline
boot_and_wait
else
# Warm simulator on CI image: configure offline, single boot.
log "Warm simulator — applying autofill offline, then single boot"
shutdown_if_booted
configure_autofill_offline
boot_and_wait
fi
seed_password_defaults
install_app
grant_notifications
if [ "${PREBOOT_SKIP_PREWARM:-}" != "1" ]; then
if ! prewarm_app "$PREWARM_SECS"; then
log "Retrying pre-warm with 25s window..."
prewarm_app 25 || log "Pre-warm failed — first Detox launch may be slow"
fi
else
log "Skipping pre-warm (PREBOOT_SKIP_PREWARM=1)"
fi
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID" || true
wait_ready_quick
verify_health
if [ -n "${GITHUB_ENV:-}" ]; then
echo "SIMULATOR_ID=$SIMULATOR_ID" >> "$GITHUB_ENV"
fi
log "Done. SIMULATOR_ID=$SIMULATOR_ID"