From e89bafcbfb53631c942170f525c840235f528e66 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 16:56:58 +0200 Subject: [PATCH 01/10] Use ATD Android emulator in CI --- .github/workflows/test.yml | 18 ++- example-app/scripts/run-maestro-android-ci.sh | 153 ------------------ 2 files changed, 15 insertions(+), 156 deletions(-) delete mode 100755 example-app/scripts/run-maestro-android-ci.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b1ebad..0bc2827 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,15 +94,27 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Setup Android SDK tools - uses: android-actions/setup-android@v3 - name: Install Maestro CLI run: | export MAESTRO_VERSION=2.4.0 curl -Ls "https://get.maestro.mobile.dev" | bash echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - name: Run Android Maestro flows - run: cd example-app && ./scripts/run-maestro-android-ci.sh + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: google_atd + arch: x86_64 + profile: pixel_6 + emulator-boot-timeout: 900 + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none + disable-animations: true + script: | + cd example-app/android + ./gradlew assembleDebug + adb install -r app/build/outputs/apk/debug/app-debug.apk + cd .. + bun run maestro:android - name: Upload Android Maestro artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/example-app/scripts/run-maestro-android-ci.sh b/example-app/scripts/run-maestro-android-ci.sh deleted file mode 100755 index c18a2ad..0000000 --- a/example-app/scripts/run-maestro-android-ci.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -ANDROID_DIR="$ROOT_DIR/android" -OUTPUT_DIR="$ROOT_DIR/build/maestro" -EMULATOR_LOG="$OUTPUT_DIR/android-emulator.log" - -ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}" - -ANDROID_API_LEVEL="${ANDROID_API_LEVEL:-34}" -ANDROID_EMULATOR_ARCH="${ANDROID_EMULATOR_ARCH:-x86_64}" -ANDROID_EMULATOR_DEVICE="${ANDROID_EMULATOR_DEVICE:-pixel_6}" -ANDROID_EMULATOR_NAME="${ANDROID_EMULATOR_NAME:-maestro-ci}" -ANDROID_EMULATOR_PORT="${ANDROID_EMULATOR_PORT:-5554}" -ANDROID_SERIAL="emulator-${ANDROID_EMULATOR_PORT}" -ANDROID_BOOT_TIMEOUT_SECONDS="${ANDROID_BOOT_TIMEOUT_SECONDS:-420}" - -mkdir -p "$OUTPUT_DIR" - -find_sdk_tool() { - local tool="$1" - local candidate - - if candidate="$(command -v "$tool" 2>/dev/null)"; then - echo "$candidate" - return 0 - fi - - for candidate in \ - "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/$tool" \ - "$ANDROID_SDK_ROOT/cmdline-tools"/*/bin/"$tool" \ - "$ANDROID_SDK_ROOT/emulator/$tool" \ - "$ANDROID_SDK_ROOT/platform-tools/$tool"; do - if [[ -x "$candidate" ]]; then - echo "$candidate" - return 0 - fi - done - - return 1 -} - -SDKMANAGER="$(find_sdk_tool sdkmanager || true)" -AVDMANAGER="$(find_sdk_tool avdmanager || true)" -EMULATOR_BIN="$(find_sdk_tool emulator || true)" -ADB_BIN="$(find_sdk_tool adb || true)" - -if [[ -z "$SDKMANAGER" || -z "$AVDMANAGER" || -z "$EMULATOR_BIN" || -z "$ADB_BIN" ]]; then - echo "Android SDK tools are missing under $ANDROID_SDK_ROOT." >&2 - exit 1 -fi - -install_system_image() { - local candidate - - yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true - - for candidate in \ - "system-images;android-${ANDROID_API_LEVEL};google_atd;${ANDROID_EMULATOR_ARCH}" \ - "system-images;android-${ANDROID_API_LEVEL};google_apis;${ANDROID_EMULATOR_ARCH}"; do - if yes | "$SDKMANAGER" --install \ - "platform-tools" \ - "emulator" \ - "platforms;android-${ANDROID_API_LEVEL}" \ - "$candidate"; then - echo "$candidate" - return 0 - fi - done - - echo "Failed to install a compatible Android system image." >&2 - return 1 -} - -wait_for_boot() { - local deadline boot_completed boot_anim - deadline=$((SECONDS + ANDROID_BOOT_TIMEOUT_SECONDS)) - - "$ADB_BIN" start-server >/dev/null - "$ADB_BIN" -s "$ANDROID_SERIAL" wait-for-device - - while ((SECONDS < deadline)); do - boot_completed="$("$ADB_BIN" -s "$ANDROID_SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" - boot_anim="$("$ADB_BIN" -s "$ANDROID_SERIAL" shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r')" - - if [[ "$boot_completed" == "1" && "$boot_anim" == "stopped" ]]; then - return 0 - fi - - sleep 5 - done - - echo "Android emulator did not boot within ${ANDROID_BOOT_TIMEOUT_SECONDS}s." >&2 - return 1 -} - -wait_for_package_manager() { - local deadline - deadline=$((SECONDS + 120)) - - while ((SECONDS < deadline)); do - if "$ADB_BIN" -s "$ANDROID_SERIAL" shell pm path android >/dev/null 2>&1; then - return 0 - fi - sleep 2 - done - - echo "Android package manager did not become ready in time." >&2 - return 1 -} - -cleanup() { - "$ADB_BIN" -s "$ANDROID_SERIAL" emu kill >/dev/null 2>&1 || true -} -trap cleanup EXIT - -SYSTEM_IMAGE_PACKAGE="$(install_system_image)" -rm -rf "$HOME/.android/avd/${ANDROID_EMULATOR_NAME}.avd" "$HOME/.android/avd/${ANDROID_EMULATOR_NAME}.ini" -echo "no" | "$AVDMANAGER" create avd \ - --force \ - --name "$ANDROID_EMULATOR_NAME" \ - --package "$SYSTEM_IMAGE_PACKAGE" \ - --device "$ANDROID_EMULATOR_DEVICE" >/dev/null - -: >"$EMULATOR_LOG" -"$EMULATOR_BIN" \ - -avd "$ANDROID_EMULATOR_NAME" \ - -port "$ANDROID_EMULATOR_PORT" \ - -no-window \ - -gpu swiftshader_indirect \ - -no-snapshot \ - -noaudio \ - -no-boot-anim \ - -camera-back none \ - -camera-front none >"$EMULATOR_LOG" 2>&1 & - -wait_for_boot -wait_for_package_manager - -"$ADB_BIN" -s "$ANDROID_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$ANDROID_SERIAL" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$ANDROID_SERIAL" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$ANDROID_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true -"$ADB_BIN" -s "$ANDROID_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true - -cd "$ANDROID_DIR" -./gradlew assembleDebug -"$ADB_BIN" -s "$ANDROID_SERIAL" install -r app/build/outputs/apk/debug/app-debug.apk - -cd "$ROOT_DIR" -export ANDROID_SERIAL -bun run maestro:android From 0fdb790b32e7f2a59805ff9517c5f67f4bab0285 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 17:00:12 +0200 Subject: [PATCH 02/10] Retrigger PR CI From 989df4a51ecef6439d31b9323b684e58651a636b Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 17:01:45 +0200 Subject: [PATCH 03/10] Allow manual CI dispatch --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bc2827..78a837d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: - renovate/** pull_request: branches: [main] + workflow_dispatch: workflow_call: # Allow this workflow to be called by other workflows jobs: From c73b54951077bbf68d34ed1ca85c95e29a8e7d2f Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 17:10:29 +0200 Subject: [PATCH 04/10] Fix Android Maestro CI working directory --- .github/workflows/test.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78a837d..3d75355 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,11 +111,9 @@ jobs: emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none disable-animations: true script: | - cd example-app/android - ./gradlew assembleDebug - adb install -r app/build/outputs/apk/debug/app-debug.apk - cd .. - bun run maestro:android + ./example-app/android/gradlew -p ./example-app/android assembleDebug + adb install -r example-app/android/app/build/outputs/apk/debug/app-debug.apk + cd example-app && bun run maestro:android - name: Upload Android Maestro artifacts if: always() uses: actions/upload-artifact@v4 From 8c6026effec5d682f54798fa73c04598f9dd4e7c Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 17:22:25 +0200 Subject: [PATCH 05/10] Avoid Maestro Android port-forward conflict --- example-app/README.md | 2 +- example-app/scripts/run-maestro-android.sh | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/example-app/README.md b/example-app/README.md index 49a2c3b..1890874 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -41,7 +41,7 @@ bun run maestro:ios bun run maestro:android ``` -The Android script bootstraps Maestro's embedded driver APKs explicitly, starts the instrumentation runner, and forwards the gRPC bridge before running the flows. That makes local Android runs match CI instead of depending on Maestro's flaky auto-bootstrap behavior. +The Android script bootstraps Maestro's embedded driver APKs explicitly and starts the instrumentation runner before running the flows. The CLI still owns the ADB port-forward setup so local runs match CI without fighting Maestro's session setup. ### Native sync diff --git a/example-app/scripts/run-maestro-android.sh b/example-app/scripts/run-maestro-android.sh index 783fe05..3483333 100755 --- a/example-app/scripts/run-maestro-android.sh +++ b/example-app/scripts/run-maestro-android.sh @@ -63,10 +63,6 @@ if [[ "${DRIVER_READY:-0}" -ne 1 ]]; then exit 1 fi -adb -s "$DEVICE_ID" forward --remove tcp:7001 >/dev/null 2>&1 || true -adb -s "$DEVICE_ID" forward tcp:7001 tcp:7001 >/dev/null -sleep 2 - JAVA_TOOL_OPTIONS=-Djava.net.preferIPv4Stack=true maestro test \ --no-reinstall-driver \ -p android \ From b0a5cd8835fad50cb7fa1124d1e1bf755270ef53 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 18:00:34 +0200 Subject: [PATCH 06/10] Document Maestro CLI prerequisite --- example-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-app/README.md b/example-app/README.md index 1890874..bc4660d 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -41,7 +41,7 @@ bun run maestro:ios bun run maestro:android ``` -The Android script bootstraps Maestro's embedded driver APKs explicitly and starts the instrumentation runner before running the flows. The CLI still owns the ADB port-forward setup so local runs match CI without fighting Maestro's session setup. +The Android script bootstraps Maestro's embedded driver APKs explicitly and starts the instrumentation runner before running the flows. The CLI still owns the ADB port-forward setup so local runs match CI without fighting Maestro's session setup. Install Maestro CLI first before running `bun run maestro:android`; the script expects the local CLI artifacts under `$HOME/.maestro`. ### Native sync From 5080c07ed1275af9e223cf37a76f713bd3e18174 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 18:08:58 +0200 Subject: [PATCH 07/10] Build iOS core in CI without LFS --- .github/workflows/test.yml | 32 +++++++++++++++++++++----------- rust/build_x264_ios_sim_arm64.sh | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d75355..5680a38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,8 +16,6 @@ jobs: steps: - name: Check out uses: actions/checkout@v6 - with: - lfs: true - name: Enforce capacitor-swift-pm version run: | set -euo pipefail @@ -31,8 +29,6 @@ jobs: steps: - name: Check out uses: actions/checkout@v6 - with: - lfs: true - uses: oven-sh/setup-bun@v2 - name: Install dependencies id: install_code @@ -48,22 +44,33 @@ jobs: id: build_code run: bun run verify:android build_ios: - timeout-minutes: 30 + timeout-minutes: 90 runs-on: macOS-latest steps: - name: Check out uses: actions/checkout@v6 with: - lfs: true + submodules: recursive + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable - uses: oven-sh/setup-bun@v2 - name: Install dependencies id: install_code run: bun i - name: Check plugin wiring run: bun run check:wiring + - name: Build native iOS core + run: cd rust && ./build_native_xcframework.sh - name: Build id: build_code run: bun run verify:ios && bun run test:macos + - name: Archive native iOS core artifact + run: tar -czf /tmp/native-ios-core-xcframework.tgz -C ios CapacitorFFmpegNativeCore.xcframework + - name: Upload native iOS core artifact + uses: actions/upload-artifact@v4 + with: + name: native-ios-core-xcframework + path: /tmp/native-ios-core-xcframework.tgz maestro_android: timeout-minutes: 60 runs-on: ubuntu-latest @@ -71,8 +78,6 @@ jobs: steps: - name: Check out uses: actions/checkout@v6 - with: - lfs: true - uses: oven-sh/setup-bun@v2 - uses: actions/setup-node@v4 with: @@ -133,8 +138,15 @@ jobs: steps: - name: Check out uses: actions/checkout@v6 + - name: Download native iOS core artifact + uses: actions/download-artifact@v4 with: - lfs: true + name: native-ios-core-xcframework + path: /tmp + - name: Restore native iOS core artifact + run: | + rm -rf ios/CapacitorFFmpegNativeCore.xcframework + tar -xzf /tmp/native-ios-core-xcframework.tgz -C ios - uses: oven-sh/setup-bun@v2 - uses: actions/setup-node@v4 with: @@ -194,8 +206,6 @@ jobs: steps: - name: Check out uses: actions/checkout@v6 - with: - lfs: true - uses: oven-sh/setup-bun@v2 - name: Install dependencies id: install_code diff --git a/rust/build_x264_ios_sim_arm64.sh b/rust/build_x264_ios_sim_arm64.sh index bdcaa53..b67f750 100755 --- a/rust/build_x264_ios_sim_arm64.sh +++ b/rust/build_x264_ios_sim_arm64.sh @@ -3,7 +3,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -X264_DIR="$SCRIPT_DIR/x264" +X264_DIR="$SCRIPT_DIR/rust/x264" OUTPUT_DIR="$SCRIPT_DIR/x264-build-ios-sim-arm64" echo "Building x264 for iOS Simulator ARM64..." From e299fa30d8c60b72169cfa4621dc41c9886ee64d Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 18:35:38 +0200 Subject: [PATCH 08/10] Pin Rust and prebuild Maestro APK --- .github/workflows/test.yml | 5 +++-- rust-toolchain.toml | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5680a38..ceffafb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: with: submodules: recursive - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.90.0 - uses: oven-sh/setup-bun@v2 - name: Install dependencies id: install_code @@ -95,6 +95,8 @@ jobs: with: distribution: 'zulu' java-version: '21' + - name: Build Android debug APK + run: ./example-app/android/gradlew -p ./example-app/android assembleDebug - name: Enable KVM access run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules @@ -116,7 +118,6 @@ jobs: emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none disable-animations: true script: | - ./example-app/android/gradlew -p ./example-app/android assembleDebug adb install -r example-app/android/app/build/outputs/apk/debug/app-debug.apk cd example-app && bun run maestro:android - name: Upload Android Maestro artifacts diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ff100ed --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.90.0" From fd14447de36ac885e01c5bb5d1267ae5120c5867 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 19:49:25 +0200 Subject: [PATCH 09/10] Stabilize Android Maestro contract flow --- .../.maestro/unsupported-reencode-android.yaml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/example-app/.maestro/unsupported-reencode-android.yaml b/example-app/.maestro/unsupported-reencode-android.yaml index 9f39682..ef06c0d 100644 --- a/example-app/.maestro/unsupported-reencode-android.yaml +++ b/example-app/.maestro/unsupported-reencode-android.yaml @@ -4,18 +4,14 @@ name: Verify unsupported Android re-encode contract - stopApp - launchApp - extendedWaitUntil: - visible: "FFmpeg capability playground" + visible: "Run runtime checks" timeout: 30000 -- tapOn: "Show video workflow" +- tapOn: "Run runtime checks" - extendedWaitUntil: - visible: "Verify unsupported path" - timeout: 30000 -- tapOn: "Verify unsupported path" -- extendedWaitUntil: - visible: "Unsupported re-encode smoke test" + visible: "Unsupported re-encode contract" timeout: 30000 - assertVisible: - text: "Unsupported re-encode smoke test" + text: "Unsupported re-encode contract" - assertVisible: - text: "passed|Passed" + text: "reencodeVideo is currently only available on iOS." - takeScreenshot: "unsupported-reencode-android-complete" From 0b460a188712c47018d48c5b91ac848c101cb2cc Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 8 Apr 2026 20:09:16 +0200 Subject: [PATCH 10/10] Harden Android image Maestro flow --- example-app/.maestro/image-conversion.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/example-app/.maestro/image-conversion.yaml b/example-app/.maestro/image-conversion.yaml index d6afa1c..42ba7f8 100644 --- a/example-app/.maestro/image-conversion.yaml +++ b/example-app/.maestro/image-conversion.yaml @@ -6,7 +6,9 @@ name: Convert image from picker - extendedWaitUntil: visible: "Load demo image" timeout: 60000 -- tapOn: "Load demo image" +- tapOn: + text: "Load demo image" + retryTapIfNoChange: true - extendedWaitUntil: visible: text: "sample-image.png staged in the app cache." @@ -20,10 +22,11 @@ name: Convert image from picker centerElement: true - tapOn: text: "Convert image" + retryTapIfNoChange: true - extendedWaitUntil: visible: - text: "Converted .* to (jpeg|webp).*" - timeout: 30000 + text: "Converted .* to (jpeg|png|webp).*" + timeout: 60000 - assertVisible: - text: "Converted .* to (jpeg|webp).*" + text: "Converted .* to (jpeg|png|webp).*" - takeScreenshot: "image-conversion-complete"