From 639b0bd90cf97c9a6e8b3235df9dacc2a01544ac Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 29 Oct 2026 10:44:22 -0700 Subject: [PATCH 1/5] feat(appium): add --install-only flag to run-local Builds, launches the device, resets, and installs the app, then exits without starting Appium or running tests. Co-Authored-By: Claude Opus 5.5 --- appium/scripts/README.md | 27 ++++++++++++++------------- appium/scripts/run-local.sh | 7 +++++++ appium/scripts/run-local/config.sh | 24 +++++++++++++++++------- appium/scripts/run-local/runtime.sh | 25 +++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/appium/scripts/README.md b/appium/scripts/README.md index ab986a7..47e4e49 100644 --- a/appium/scripts/README.md +++ b/appium/scripts/README.md @@ -75,19 +75,20 @@ If `--platform` or `--sdk` are not provided, the script prompts interactively. ### Options -| Flag | Description | -| --------------- | ---------------------------------------------------------------------------------------------- | -| `--platform=P` | `ios` or `android` | -| `--sdk=S` | `flutter`, `react-native`, `cordova`, `capacitor`, `expo`, `dotnet`, `unity`, `android`, `ios` | -| `--spec=GLOB` | Spec file glob (default: `tests/specs/**/*.spec.ts`) | -| `--skip` | Skip build, device launch, and app reset (rerun tests only) | -| `--skip-build` | Skip app build (reuse existing `.app`/`.apk`) | -| `--skip-device` | Skip simulator/emulator launch | -| `--skip-reset` | Keep existing app data between runs | -| `--pods` | Use `examples/demo-pods` for Flutter, Cordova, and Capacitor | -| `--release` | Check out the latest release point in each SDK repo first | -| `--main` | Check out the latest `main` in each SDK repo first | -| `-h, --help` | Show help | +| Flag | Description | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| `--platform=P` | `ios` or `android` | +| `--sdk=S` | `flutter`, `react-native`, `cordova`, `capacitor`, `expo`, `dotnet`, `unity`, `android`, `ios` | +| `--spec=GLOB` | Spec file glob (default: `tests/specs/**/*.spec.ts`) | +| `--skip` | Skip build, device launch, and app reset (rerun tests only) | +| `--skip-build` | Skip app build (reuse existing `.app`/`.apk`) | +| `--skip-device` | Skip simulator/emulator launch | +| `--skip-reset` | Keep existing app data between runs | +| `--install-only` | Build, launch the device, reset, and install the app; skip Appium and tests | +| `--pods` | Use `examples/demo-pods` for Flutter, Cordova, and Capacitor | +| `--release` | Check out the latest release point in each SDK repo first | +| `--main` | Check out the latest `main` in each SDK repo first | +| `-h, --help` | Show help | ### Examples diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index e63ef42..7fa264d 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -84,6 +84,13 @@ main() { begin_quiet_setup build_app + if [[ "$INSTALL_ONLY" == true ]]; then + prepare_install_only + end_quiet_setup + echo "" + info "=== Installed (--install-only, tests skipped) ===" + return + fi prepare_runtime install_test_deps end_quiet_setup diff --git a/appium/scripts/run-local/config.sh b/appium/scripts/run-local/config.sh index b7200fe..84cc1c5 100644 --- a/appium/scripts/run-local/config.sh +++ b/appium/scripts/run-local/config.sh @@ -8,6 +8,7 @@ configure_runner() { SKIP_BUILD=false SKIP_DEVICE=false SKIP_RESET=false + INSTALL_ONLY=false SPEC="" QUIET=false PODS_DEMO=false @@ -33,6 +34,7 @@ configure_runner() { --skip-build) SKIP_BUILD=true ;; --skip-device) SKIP_DEVICE=true ;; --skip-reset) SKIP_RESET=true ;; + --install-only) INSTALL_ONLY=true ;; --wipe-emulator) WIPE_EMULATOR=true ;; --pods) PODS_DEMO=true ;; --release) RELEASE=true ;; @@ -66,6 +68,8 @@ Options: --skip-build Skip app build (reuse existing) --skip-device Skip simulator/emulator launch --skip-reset Keep existing app data + --install-only Build, launch the device, reset, and install the app, + then exit without starting Appium or running tests --wipe-emulator Wipe the Android AVD before launch. Deletes all emulator apps and data; incompatible with --skip-device. --pods Use examples/demo-pods instead of examples/demo for @@ -198,6 +202,19 @@ USAGE # calls `vpx wdio run` directly on BrowserStack — so these checks are # local-only by construction. preflight() { + if [[ "$INSTALL_ONLY" != true ]]; then + preflight_test_tooling + fi + + if [[ -z "${ONESIGNAL_APP_ID:-}" || -z "${ONESIGNAL_API_KEY:-}" ]]; then + error "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set. Use the OneSignal app + dedicated to Appium tests (not a general/shared app — its live in-app + marketing campaigns can cover the UI and cause misleading 'element not + displayed' failures). Set both in $SCRIPT_DIR/.env (cp .env.example .env)." + fi + } + + preflight_test_tooling() { command -v appium >/dev/null 2>&1 \ || error "appium not found on PATH. Install it with: npm i -g appium" @@ -239,13 +256,6 @@ USAGE export WDIO_USE_NATIVE_FETCH=1 info "Node $node_major detected — setting WDIO_USE_NATIVE_FETCH=1 (works around webdriverio's undici dispatcher being rejected by Node 26+ fetch)." fi - - if [[ -z "${ONESIGNAL_APP_ID:-}" || -z "${ONESIGNAL_API_KEY:-}" ]]; then - error "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set. Use the OneSignal app - dedicated to Appium tests (not a general/shared app — its live in-app - marketing campaigns can cover the UI and cause misleading 'element not - displayed' failures). Set both in $SCRIPT_DIR/.env (cp .env.example .env)." - fi } preflight diff --git a/appium/scripts/run-local/runtime.sh b/appium/scripts/run-local/runtime.sh index 4d86a4c..3ef8f91 100644 --- a/appium/scripts/run-local/runtime.sh +++ b/appium/scripts/run-local/runtime.sh @@ -399,3 +399,28 @@ prepare_runtime() { cleanup_android_automation reset_app } + +# Appium installs the app at session start during test runs, so install-only +# has to do it explicitly. No -g on adb: tests expect the permission prompt. +install_app() { + info "Installing $APP_PATH..." + if [[ "$PLATFORM" == "android" ]]; then + adb -s "$ANDROID_UDID" install -r "$APP_PATH" >/dev/null \ + || error "Failed to install $APP_PATH on $ANDROID_UDID" + elif [[ "$IOS_REAL_DEVICE" == true ]]; then + xcrun devicectl device install app --device "$UDID" "$APP_PATH" >/dev/null \ + || error "Failed to install $APP_PATH on device $UDID" + else + xcrun simctl install "${UDID:-booted}" "$APP_PATH" \ + || error "Failed to install $APP_PATH on simulator ${UDID:-booted}" + fi + info "Installed $BUNDLE_ID" +} + +prepare_install_only() { + validate_existing_app + start_device + check_android_storage + reset_app + install_app +} From 328f6c30f517672ac31bdba3c9b31d6832eadd44 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 29 Oct 2026 11:08:35 -0700 Subject: [PATCH 2/5] feat(appium): add --pick-device to choose simulator/emulator Prompts for an iOS simulator and/or Android AVD in run-local.sh and, up front, in run-all.sh. Also lets --device override .env device names and syncs OS_VERSION with the running device. Co-Authored-By: Claude Opus 5.5 --- appium/scripts/README.md | 1 + appium/scripts/run-all.sh | 33 +++++++++ appium/scripts/run-local.sh | 1 + appium/scripts/run-local/config.sh | 14 +++- appium/scripts/run-local/device-picker.sh | 81 +++++++++++++++++++++++ appium/scripts/run-local/runtime.sh | 23 +++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 appium/scripts/run-local/device-picker.sh diff --git a/appium/scripts/README.md b/appium/scripts/README.md index 47e4e49..7a44257 100644 --- a/appium/scripts/README.md +++ b/appium/scripts/README.md @@ -85,6 +85,7 @@ If `--platform` or `--sdk` are not provided, the script prompts interactively. | `--skip-device` | Skip simulator/emulator launch | | `--skip-reset` | Keep existing app data between runs | | `--install-only` | Build, launch the device, reset, and install the app; skip Appium and tests | +| `--pick-device` | Prompt for which iOS simulator or Android emulator to use | | `--pods` | Use `examples/demo-pods` for Flutter, Cordova, and Capacitor | | `--release` | Check out the latest release point in each SDK repo first | | `--main` | Check out the latest `main` in each SDK repo first | diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index 2695d1b..b54012b 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -11,6 +11,7 @@ if [[ -f "$SCRIPT_DIR/.env" ]]; then fi source "$SCRIPT_DIR/run-local/repo-dir.sh" +source "$SCRIPT_DIR/run-local/device-picker.sh" # ── Colors / logging ───────────────────────────────────────────────────────── RED='\033[0;31m' @@ -34,6 +35,7 @@ PODS_REQUESTED=0 RELEASE=0 MAIN=0 WIPE_EMULATOR=0 +PICK_DEVICE=0 for arg in "$@"; do case "$arg" in --skip-build|--skip-device|--skip-reset|--skip|--quiet|-q) @@ -57,6 +59,8 @@ for arg in "$@"; do SDKS_FILTER="${arg#*=}" ;; --bail) BAIL=1 ;; + --pick-device) + PICK_DEVICE=1 ;; --help|-h) cat < 0 )); then echo -e "${BOLD}━━━ Checking out latest releases ━━━${NC}" if ! "$SCRIPT_DIR/checkout-releases.sh" "${RUN_SDKS[@]}"; then @@ -229,6 +257,11 @@ for platform in "${PLATFORMS[@]}"; do flutter|cordova|capacitor) combo_args+=(--pods) ;; esac fi + if [[ "$platform" == "ios" ]]; then + combo_args+=(${IOS_DEVICE_ARGS[@]+"${IOS_DEVICE_ARGS[@]}"}) + else + combo_args+=(${ANDROID_DEVICE_ARGS[@]+"${ANDROID_DEVICE_ARGS[@]}"}) + fi if [[ "$platform" == "android" ]] && (( WIPE_EMULATOR )) && (( ! ANDROID_EMULATOR_WIPED )); then combo_args+=(--wipe-emulator) ANDROID_EMULATOR_WIPED=1 diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 7fa264d..dc1391c 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -16,6 +16,7 @@ RUN_LOCAL_LIB_DIR="$SCRIPT_DIR/run-local" source "$RUN_LOCAL_LIB_DIR/common.sh" source "$RUN_LOCAL_LIB_DIR/repo-dir.sh" source "$RUN_LOCAL_LIB_DIR/config.sh" +source "$RUN_LOCAL_LIB_DIR/device-picker.sh" source "$RUN_LOCAL_LIB_DIR/sdk-artifacts.sh" source "$RUN_LOCAL_LIB_DIR/build.sh" source "$RUN_LOCAL_LIB_DIR/runtime.sh" diff --git a/appium/scripts/run-local/config.sh b/appium/scripts/run-local/config.sh index 84cc1c5..20667e2 100644 --- a/appium/scripts/run-local/config.sh +++ b/appium/scripts/run-local/config.sh @@ -9,6 +9,7 @@ configure_runner() { SKIP_DEVICE=false SKIP_RESET=false INSTALL_ONLY=false + PICK_DEVICE=false SPEC="" QUIET=false PODS_DEMO=false @@ -24,7 +25,8 @@ configure_runner() { case "$arg" in --platform=*) PLATFORM="${arg#--platform=}" ;; --sdk=*) SDK_TYPE="${arg#--sdk=}" ;; - --device=*) DEVICE="${arg#--device=}" ;; + --device=*) DEVICE="${arg#--device=}"; AVD_NAME=""; IOS_SIMULATOR="" ;; + --pick-device) PICK_DEVICE=true ;; --device-real) IOS_REAL_DEVICE=true; SKIP_DEVICE=true ;; --udid=*) UDID="${arg#--udid=}" ;; --appium-port=*) APPIUM_PORT="${arg#--appium-port=}" ;; @@ -59,6 +61,7 @@ Options: ios = native iOS (OneSignal-iOS-SDK/examples/demo); skips with exit 0 when --platform=android. --device=NAME Device/simulator/AVD name (default: iPhone 17 / Android 17) + --pick-device Prompt for which iOS simulator or Android emulator to use --appium-port=N Appium server port (default: 4723). Use unique values when running multiple sessions in parallel on the same host. --system-port=N UiAutomator2 systemPort (Android only). Required when @@ -180,6 +183,15 @@ USAGE error "--release and --main are mutually exclusive" fi + if [[ "$PICK_DEVICE" == true ]]; then + [[ "$IOS_REAL_DEVICE" == false ]] || error "--pick-device cannot be combined with --device-real" + pick_device "$PLATFORM" || error "No $PLATFORM device to pick from" + DEVICE="$PICKED_DEVICE" + AVD_NAME="" + IOS_SIMULATOR="" + [[ "$PLATFORM" == "ios" ]] && UDID="$PICKED_UDID" + fi + resolve_sdk_dir "$SDK_TYPE" if [[ "$RELEASE" == true ]]; then diff --git a/appium/scripts/run-local/device-picker.sh b/appium/scripts/run-local/device-picker.sh new file mode 100644 index 0000000..d9d4434 --- /dev/null +++ b/appium/scripts/run-local/device-picker.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# Prompts for an iOS simulator or Android AVD. Sets PICKED_DEVICE (simulator +# or AVD name) and, for iOS, PICKED_UDID, since simulator names repeat across +# runtimes. +pick_device() { + local platform="$1" + local -a names=() udids=() labels=() + local name udid label + PICKED_DEVICE="" + PICKED_UDID="" + + if [[ "$platform" == "ios" ]]; then + while IFS='|' read -r udid name label; do + [[ -n "$udid" ]] || continue + udids+=("$udid") + names+=("$name") + labels+=("$label") + done < <(xcrun simctl list devices available -j 2>/dev/null | python3 -c ' +import json, sys +data = json.load(sys.stdin) +rows = [] +for runtime, devices in data["devices"].items(): + rt = runtime.rsplit(".", 1)[-1] + if not rt.startswith("iOS-"): + continue + try: + ver = tuple(int(p) for p in rt[4:].split("-")) + except ValueError: + continue + for d in devices: + label = d["name"] + " (iOS " + ".".join(map(str, ver)) + ")" + if d["state"] == "Booted": + label += " [booted]" + rows.append((ver, d["name"], d["udid"], label)) +for ver, name, udid, label in sorted(rows, key=lambda r: (tuple(-v for v in r[0]), not r[1].startswith("iPhone"), r[1])): + print(udid + "|" + name + "|" + label) +' 2>/dev/null) + else + local running + running=$(ps -axo command= 2>/dev/null | grep -oE -- '-avd [^ ]+' | awk '{print $2}' || true) + while read -r name; do + [[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || continue + label="$name" + grep -qxF -- "$name" <<<"$running" && label+=" [running]" + names+=("$name") + labels+=("$label") + done < <(emulator -list-avds 2>/dev/null) + fi + + if (( ${#names[@]} == 0 )); then + if [[ "$platform" == "ios" ]]; then + echo "No iOS simulators found (xcrun simctl list devices available)" >&2 + else + echo "No Android AVDs found (emulator -list-avds)" >&2 + fi + return 1 + fi + + echo "" + if [[ "$platform" == "ios" ]]; then + echo -e "${GREEN}Select iOS simulator:${NC}" + else + echo -e "${GREEN}Select Android emulator:${NC}" + fi + local i + for i in "${!labels[@]}"; do + echo " $((i + 1))) ${labels[$i]}" + done + + local choice + while true; do + read -rp "> " choice + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#names[@]} )); then + PICKED_DEVICE="${names[$((choice - 1))]}" + [[ "$platform" == "ios" ]] && PICKED_UDID="${udids[$((choice - 1))]}" + return 0 + fi + echo " Invalid choice. Enter a number 1-${#names[@]}." + done +} diff --git a/appium/scripts/run-local/runtime.sh b/appium/scripts/run-local/runtime.sh index 3ef8f91..b86da40 100644 --- a/appium/scripts/run-local/runtime.sh +++ b/appium/scripts/run-local/runtime.sh @@ -390,9 +390,31 @@ validate_existing_app() { fi } +# Appium matches platformVersion against the running device, so a picked or +# --device/--udid target must not keep the default OS_VERSION. +sync_device_os_version() { + local version="" + if [[ "$PLATFORM" == "android" ]]; then + [[ -n "${ANDROID_UDID:-}" ]] || return 0 + version=$(adb_with_timeout 10 -s "$ANDROID_UDID" shell getprop ro.build.version.release 2>/dev/null | tr -d '\r' || true) + else + [[ "$IOS_REAL_DEVICE" == false && -n "$UDID" ]] || return 0 + version=$(xcrun simctl list devices -j 2>/dev/null | python3 -c " +import json, sys +for runtime, devices in json.load(sys.stdin)['devices'].items(): + if any(d['udid'] == '$UDID' for d in devices): + print(runtime.rsplit('.', 1)[-1].replace('iOS-', '').replace('-', '.')) +" 2>/dev/null || true) + fi + [[ "$version" =~ ^[0-9]+(\.[0-9]+)*$ && "$version" != "$OS_VERSION" ]] || return 0 + info "Using OS_VERSION=$version to match the running device (was $OS_VERSION)" + OS_VERSION="$version" +} + prepare_runtime() { validate_existing_app start_device + sync_device_os_version check_android_storage install_chromedriver_if_needed start_appium @@ -420,6 +442,7 @@ install_app() { prepare_install_only() { validate_existing_app start_device + sync_device_os_version check_android_storage reset_app install_app From d99ae077e3c715b5558016a13813fe16dae4f4a7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 29 Oct 2026 15:00:00 -0700 Subject: [PATCH 3/5] fix(appium): address review feedback on install/pick-device flags - Fail the device picker on EOF instead of looping - Make --device fully override .env on iOS (UDID, runtime) - Forward --install-only from run-all, one SDK per platform - Resolve the booted simulator instead of simctl's `booted` target - Warn when the device OS version can't be read - Extract ios_sim_version and combo_runs helpers Co-Authored-By: Claude Opus 5.5 --- appium/scripts/README.md | 4 +- appium/scripts/run-all.sh | 53 +++++++++------ appium/scripts/run-local/config.sh | 20 ++++-- appium/scripts/run-local/device-picker.sh | 6 +- appium/scripts/run-local/runtime.sh | 79 +++++++++++++++++------ 5 files changed, 118 insertions(+), 44 deletions(-) diff --git a/appium/scripts/README.md b/appium/scripts/README.md index 7a44257..e9fb3de 100644 --- a/appium/scripts/README.md +++ b/appium/scripts/README.md @@ -1,6 +1,6 @@ # Local Test Runner -`run-local.sh` builds the app, boots a simulator/emulator, starts Appium, resets app state, and runs the E2E test suite. +`run-local.sh` builds the app, boots a simulator/emulator, starts Appium, resets app state, and runs the E2E test suite. With `--install-only` it stops after installing the app. ## Setup @@ -146,6 +146,8 @@ Skip only the build (simulator + reset still happen): ./run-all.sh --release # check out the latest release point per repo first ./run-all.sh --main # check out the latest main per repo first ./run-all.sh --bail # stop after the first failing combo +./run-all.sh --pick-device # prompt for a simulator and/or emulator up front +./run-all.sh --sdks=flutter --install-only # build and install on both platforms, no tests ``` `--release` is available on both `run-all.sh` and `run-local.sh`. It runs `checkout-releases.sh`, which checks out the newest stable `rel/X.Y.Z` branch (for expo/ios, the newest semver tag unless a newer stable `rel/X.Y.Z` branch exists for a pending release) in each SDK repo, honoring the `*_DIR` overrides from `.env`. Uncommitted changes, including untracked files, are stashed before checkout and can be restored later with `git stash pop`. diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index b54012b..450ae1b 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -36,10 +36,14 @@ RELEASE=0 MAIN=0 WIPE_EMULATOR=0 PICK_DEVICE=0 +INSTALL_ONLY=0 for arg in "$@"; do case "$arg" in --skip-build|--skip-device|--skip-reset|--skip|--quiet|-q) EXTRA_ARGS+=("$arg") ;; + --install-only) + INSTALL_ONLY=1 + EXTRA_ARGS+=("$arg") ;; --wipe-emulator) WIPE_EMULATOR=1 ;; --pods) @@ -92,6 +96,8 @@ Options forwarded to run-local.sh: --skip-device Skip simulator/emulator launch --skip-reset Keep existing app data --skip Shortcut for --skip-build --skip-device --skip-reset + --install-only Build and install without running tests. Allows one SDK + per platform, since every demo shares one bundle id. --wipe-emulator Wipe the Android AVD before the first Android combo --pods Use examples/demo-pods for flutter, cordova, capacitor (ignored for other SDKs) @@ -148,18 +154,37 @@ if (( PODS_REQUESTED )); then fi fi -# Native SDKs only run on their own platform; drop them up front so -# --release/--main don't touch or prompt for repos that won't run. +# Native SDKs only run on their own platform. +combo_runs() { + local sdk="$1" platform="$2" + [[ ! ( "$sdk" == android && "$platform" == ios ) \ + && ! ( "$sdk" == ios && "$platform" == android ) ]] +} + +# Drop native SDKs up front so --release/--main don't touch or prompt for +# repos that won't run. if [[ -n "$PLATFORM_FILTER" ]]; then RUN_SDKS=() for sdk in "${SDKS[@]}"; do - [[ ( "$sdk" == android && "$PLATFORM_FILTER" == ios ) \ - || ( "$sdk" == ios && "$PLATFORM_FILTER" == android ) ]] || RUN_SDKS+=("$sdk") + combo_runs "$sdk" "$PLATFORM_FILTER" && RUN_SDKS+=("$sdk") done else RUN_SDKS=("${SDKS[@]}") fi +if (( INSTALL_ONLY )); then + for platform in "${PLATFORMS[@]}"; do + platform_sdks=() + for sdk in ${RUN_SDKS[@]+"${RUN_SDKS[@]}"}; do + combo_runs "$sdk" "$platform" && platform_sdks+=("$sdk") + done + if (( ${#platform_sdks[@]} > 1 )); then + error "--install-only would install ${platform_sdks[*]} on $platform, but every demo shares one bundle id so only the last would remain. Narrow with --sdks= or --platform=." + exit 2 + fi + done +fi + # --release/--main switch the default checkout to a fixed ref, so there is no # worktree to choose. if (( RELEASE || MAIN )); then @@ -178,14 +203,10 @@ if (( PICK_DEVICE )); then for platform in "${PLATFORMS[@]}"; do has_combo=0 for sdk in ${RUN_SDKS[@]+"${RUN_SDKS[@]}"}; do - [[ ( "$sdk" == android && "$platform" == ios ) \ - || ( "$sdk" == ios && "$platform" == android ) ]] || { has_combo=1; break; } + combo_runs "$sdk" "$platform" && { has_combo=1; break; } done (( has_combo )) || continue - if ! pick_device "$platform"; then - error "No $platform device to pick from" - exit 2 - fi + pick_device "$platform" || exit 2 if [[ "$platform" == "ios" ]]; then IOS_DEVICE_ARGS=(--device="$PICKED_DEVICE" --udid="$PICKED_UDID") else @@ -229,17 +250,9 @@ for platform in "${PLATFORMS[@]}"; do else label="${sdk} / ${platform}" fi - if [[ "$sdk" == "android" && "$platform" == "ios" ]]; then - if [[ -n "$PLATFORM_FILTER" ]]; then - warn "--sdk=android only runs on --platform=android; skipping --platform=ios" - RESULTS+=("SKIP ${label}") - SKIPPED=$((SKIPPED + 1)) - fi - continue - fi - if [[ "$sdk" == "ios" && "$platform" == "android" ]]; then + if ! combo_runs "$sdk" "$platform"; then if [[ -n "$PLATFORM_FILTER" ]]; then - warn "--sdk=ios only runs on --platform=ios; skipping --platform=android" + warn "--sdk=$sdk only runs on --platform=$sdk; skipping --platform=$platform" RESULTS+=("SKIP ${label}") SKIPPED=$((SKIPPED + 1)) fi diff --git a/appium/scripts/run-local/config.sh b/appium/scripts/run-local/config.sh index 20667e2..6dab8a1 100644 --- a/appium/scripts/run-local/config.sh +++ b/appium/scripts/run-local/config.sh @@ -10,6 +10,8 @@ configure_runner() { SKIP_RESET=false INSTALL_ONLY=false PICK_DEVICE=false + DEVICE_FLAG=false + UDID_FLAG=false SPEC="" QUIET=false PODS_DEMO=false @@ -25,10 +27,10 @@ configure_runner() { case "$arg" in --platform=*) PLATFORM="${arg#--platform=}" ;; --sdk=*) SDK_TYPE="${arg#--sdk=}" ;; - --device=*) DEVICE="${arg#--device=}"; AVD_NAME=""; IOS_SIMULATOR="" ;; + --device=*) DEVICE="${arg#--device=}"; DEVICE_FLAG=true ;; --pick-device) PICK_DEVICE=true ;; --device-real) IOS_REAL_DEVICE=true; SKIP_DEVICE=true ;; - --udid=*) UDID="${arg#--udid=}" ;; + --udid=*) UDID="${arg#--udid=}"; UDID_FLAG=true ;; --appium-port=*) APPIUM_PORT="${arg#--appium-port=}" ;; --wda-local-port=*) WDA_LOCAL_PORT="${arg#--wda-local-port=}" ;; --system-port=*) SYSTEM_PORT="${arg#--system-port=}" ;; @@ -135,6 +137,16 @@ USAGE esac done + # A CLI device beats device settings from .env/env. Without a pinned runtime, + # start_ios_simulator matches the name on the newest runtime that has it. + if [[ "$DEVICE_FLAG" == true ]]; then + AVD_NAME="" + IOS_SIMULATOR="" + IOS_RUNTIME="" + IOS_RUNTIME_ANY=true + [[ "$UDID_FLAG" == true ]] || UDID="" + fi + # Ensure values set via CLI flags propagate to wdio (which reads them as env). export APPIUM_PORT [[ -n "$WDA_LOCAL_PORT" ]] && export WDA_LOCAL_PORT @@ -185,7 +197,7 @@ USAGE if [[ "$PICK_DEVICE" == true ]]; then [[ "$IOS_REAL_DEVICE" == false ]] || error "--pick-device cannot be combined with --device-real" - pick_device "$PLATFORM" || error "No $PLATFORM device to pick from" + pick_device "$PLATFORM" || exit 1 DEVICE="$PICKED_DEVICE" AVD_NAME="" IOS_SIMULATOR="" @@ -445,7 +457,7 @@ USAGE DEVICE="${DEVICE:-iPhone 17}" OS_VERSION="${OS_VERSION:-26.5}" IOS_SIMULATOR="${IOS_SIMULATOR:-$DEVICE}" - IOS_RUNTIME="${IOS_RUNTIME:-iOS-26-5}" + [[ "${IOS_RUNTIME_ANY:-false}" == true ]] || IOS_RUNTIME="${IOS_RUNTIME:-iOS-26-5}" else DEVICE="${DEVICE:-Android 17}" OS_VERSION="${OS_VERSION:-17}" diff --git a/appium/scripts/run-local/device-picker.sh b/appium/scripts/run-local/device-picker.sh index d9d4434..32c3ff6 100644 --- a/appium/scripts/run-local/device-picker.sh +++ b/appium/scripts/run-local/device-picker.sh @@ -70,7 +70,11 @@ for ver, name, udid, label in sorted(rows, key=lambda r: (tuple(-v for v in r[0] local choice while true; do - read -rp "> " choice + if ! read -rp "> " choice; then + echo "" >&2 + echo "No input available; cannot pick a device" >&2 + return 1 + fi if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#names[@]} )); then PICKED_DEVICE="${names[$((choice - 1))]}" [[ "$platform" == "ios" ]] && PICKED_UDID="${udids[$((choice - 1))]}" diff --git a/appium/scripts/run-local/runtime.sh b/appium/scripts/run-local/runtime.sh index b86da40..c0c2ea0 100644 --- a/appium/scripts/run-local/runtime.sh +++ b/appium/scripts/run-local/runtime.sh @@ -23,23 +23,30 @@ start_ios_simulator() { fi local udid + # An empty IOS_RUNTIME matches any runtime; newest wins. udid=$(xcrun simctl list devices available -j \ - | python3 -c " + | python3 -c ' import json, sys +runtime_filter, name = sys.argv[1], sys.argv[2] data = json.load(sys.stdin) -for runtime, devices in data['devices'].items(): - if '$IOS_RUNTIME' in runtime: - for d in devices: - if d['name'] == '$IOS_SIMULATOR' and d['isAvailable']: - print(d['udid']); sys.exit(0) -" 2>/dev/null || true) +def version(runtime): + try: + return tuple(int(p) for p in runtime.rsplit(".", 1)[-1].replace("iOS-", "").split("-")) + except ValueError: + return () +for runtime in sorted(data["devices"], key=version, reverse=True): + if runtime_filter in runtime: + for d in data["devices"][runtime]: + if d["name"] == name and d["isAvailable"]: + print(d["udid"]); sys.exit(0) +' "$IOS_RUNTIME" "$IOS_SIMULATOR" 2>/dev/null || true) # Requested device/runtime isn't installed on this machine (e.g. defaults # assume iOS 26.5 but only an older runtime is installed). Fall back to the booted # simulator if there is one, else the newest installed iOS runtime, and # align DEVICE/OS_VERSION so the Appium session targets what actually runs. if [[ -z "$udid" ]]; then - warn "Simulator '$IOS_SIMULATOR' ($IOS_RUNTIME) not found on this machine." + warn "Simulator '$IOS_SIMULATOR' (${IOS_RUNTIME:-any runtime}) not found on this machine." local fallback fallback=$(xcrun simctl list devices -j \ | python3 -c " @@ -263,6 +270,7 @@ start_device() { || error "Android emulator '$AVD_NAME' is not running; remove --skip-device or start it first" pin_android_emulator "$emulator_udid" fi + [[ "$PLATFORM" == "ios" ]] && resolve_booted_simulator info "Skipping device launch (--skip-device)" return fi @@ -390,6 +398,42 @@ validate_existing_app() { fi } +ios_sim_version() { + xcrun simctl list devices -j 2>/dev/null | python3 -c ' +import json, sys +for runtime, devices in json.load(sys.stdin)["devices"].items(): + if any(d["udid"] == sys.argv[1] for d in devices): + print(runtime.rsplit(".", 1)[-1].replace("iOS-", "").replace("-", ".")) +' "$1" 2>/dev/null || true +} + +# With --skip-device, pin the already-booted simulator so reset and install +# don't hit simctl's ambiguous `booted` target. Prefers a name match. +resolve_booted_simulator() { + [[ "$IOS_REAL_DEVICE" == false && -z "$UDID" ]] || return 0 + local -a booted=() + local udid + while read -r udid; do + [[ -n "$udid" ]] && booted+=("$udid") + done < <(xcrun simctl list devices booted -j 2>/dev/null | python3 -c ' +import json, sys +booted = [d for ds in json.load(sys.stdin)["devices"].values() for d in ds if d["state"] == "Booted"] +named = [d for d in booted if d["name"] == sys.argv[1]] +for d in named or booted: + print(d["udid"]) +' "${IOS_SIMULATOR:-$DEVICE}" 2>/dev/null) + + if (( ${#booted[@]} == 1 )); then + UDID="${booted[0]}" + info "Using booted simulator $UDID" + elif [[ "$INSTALL_ONLY" == true ]]; then + if (( ${#booted[@]} == 0 )); then + error "No booted simulator; remove --skip-device or boot one first" + fi + error "Multiple simulators booted; pass --udid or --pick-device to choose one" + fi +} + # Appium matches platformVersion against the running device, so a picked or # --device/--udid target must not keep the default OS_VERSION. sync_device_os_version() { @@ -399,14 +443,13 @@ sync_device_os_version() { version=$(adb_with_timeout 10 -s "$ANDROID_UDID" shell getprop ro.build.version.release 2>/dev/null | tr -d '\r' || true) else [[ "$IOS_REAL_DEVICE" == false && -n "$UDID" ]] || return 0 - version=$(xcrun simctl list devices -j 2>/dev/null | python3 -c " -import json, sys -for runtime, devices in json.load(sys.stdin)['devices'].items(): - if any(d['udid'] == '$UDID' for d in devices): - print(runtime.rsplit('.', 1)[-1].replace('iOS-', '').replace('-', '.')) -" 2>/dev/null || true) + version=$(ios_sim_version "$UDID") fi - [[ "$version" =~ ^[0-9]+(\.[0-9]+)*$ && "$version" != "$OS_VERSION" ]] || return 0 + if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then + warn "Could not read the device OS version; keeping OS_VERSION=$OS_VERSION (Appium may reject a mismatched platformVersion)" + return 0 + fi + [[ "$version" != "$OS_VERSION" ]] || return 0 info "Using OS_VERSION=$version to match the running device (was $OS_VERSION)" OS_VERSION="$version" } @@ -433,8 +476,9 @@ install_app() { xcrun devicectl device install app --device "$UDID" "$APP_PATH" >/dev/null \ || error "Failed to install $APP_PATH on device $UDID" else - xcrun simctl install "${UDID:-booted}" "$APP_PATH" \ - || error "Failed to install $APP_PATH on simulator ${UDID:-booted}" + [[ -n "$UDID" ]] || error "No simulator selected for install; pass --udid or --pick-device" + xcrun simctl install "$UDID" "$APP_PATH" \ + || error "Failed to install $APP_PATH on simulator $UDID" fi info "Installed $BUNDLE_ID" } @@ -442,7 +486,6 @@ install_app() { prepare_install_only() { validate_existing_app start_device - sync_device_os_version check_android_storage reset_app install_app From 3897959a75caac49e5b86a60dfb6ae361f2e0e1b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 29 Oct 2026 15:49:47 -0700 Subject: [PATCH 4/5] fix(appium): pass simulator name to fallback lookup as argv Co-Authored-By: Claude Opus 5.5 --- appium/scripts/run-local/runtime.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appium/scripts/run-local/runtime.sh b/appium/scripts/run-local/runtime.sh index c0c2ea0..d82ea02 100644 --- a/appium/scripts/run-local/runtime.sh +++ b/appium/scripts/run-local/runtime.sh @@ -79,12 +79,12 @@ for runtime, devices in data['devices'].items(): if avail: runtimes.append((ver, rt, avail)) for ver, rt, avail in sorted(runtimes, reverse=True): - exact = [d for d in avail if d['name'] == '$IOS_SIMULATOR'] + exact = [d for d in avail if d['name'] == sys.argv[1]] iphones = sorted((d for d in avail if d['name'].startswith('iPhone')), key=lambda d: d['name']) pick = exact[0] if exact else (iphones[-1] if iphones else avail[0]) print(pick['udid'] + '|' + pick['name'] + '|' + rt + '|' + '.'.join(str(p) for p in ver)) sys.exit(0) -" 2>/dev/null || true) +" "$IOS_SIMULATOR" 2>/dev/null || true) fi if [[ -z "$fallback" ]]; then error "No usable iOS simulator found. Run: xcrun simctl list devices available, then set DEVICE / OS_VERSION / IOS_RUNTIME in $SCRIPT_DIR/.env" From a8547f8495c9dce4e6f5b6bdef5e548a811f69ff Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 29 Oct 2026 15:51:22 -0700 Subject: [PATCH 5/5] chore(ci): stop running CodeQL on pull requests Co-Authored-By: Claude Opus 5.5 --- .github/workflows/codeql.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fd8e268..eb4f77f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,8 +3,6 @@ name: "CodeQL" on: push: branches: ["main"] - pull_request: - branches: ["main"] schedule: - cron: "16 16 * * 0"