android workflow #21
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Maestro E2E Android | |
| on: | |
| push: | |
| branches: | |
| - master | |
| pull_request: | |
| branches: | |
| - master | |
| types: [opened, synchronize, reopened] | |
| workflow_dispatch: | |
| jobs: | |
| e2e-android: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 75 | |
| env: | |
| ANDROID_HOME: /home/runner/androidsdk | |
| ANDROID_SDK_ROOT: /home/runner/androidsdk | |
| ANDROID_AVD_HOME: /mnt/avd | |
| ANDROID_OS_IMAGE: system-images;android-32;google_apis;x86_64 | |
| ANDROID_PLATFORM: platforms;android-36 | |
| APP_ID: com.anonymous.client | |
| steps: | |
| - name: Enable KVM group perms | |
| run: | | |
| 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: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Inject Google API key for Expo build | |
| run: | | |
| if [ -n "${{ secrets.EXPO_PUBLIC_GOOGLE_API_KEY }}" ]; then | |
| echo "EXPO_PUBLIC_GOOGLE_API_KEY=${{ secrets.EXPO_PUBLIC_GOOGLE_API_KEY }}" >> "$GITHUB_ENV" | |
| elif [ -n "${{ secrets.GOOGLE_API_KEY }}" ]; then | |
| echo "EXPO_PUBLIC_GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> "$GITHUB_ENV" | |
| else | |
| echo "ERROR: Missing GitHub secret. Set EXPO_PUBLIC_GOOGLE_API_KEY (preferred) or GOOGLE_API_KEY." >&2 | |
| exit 1 | |
| fi | |
| - name: Free disk space | |
| run: | | |
| df -h | |
| sudo rm -rf /usr/share/dotnet || true | |
| sudo rm -rf /opt/ghc || true | |
| sudo rm -rf /usr/local/.ghcup || true | |
| sudo rm -rf /opt/hostedtoolcache/CodeQL || true | |
| sudo docker system prune -af || true | |
| sudo mkdir -p /mnt/avd | |
| sudo chmod -R 777 /mnt/avd | |
| df -h | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20.19.4" | |
| cache: npm | |
| - name: Install JS dependencies | |
| run: npm ci --ignore-scripts --prefer-offline --no-audit | |
| - name: Ensure Android native project | |
| run: | | |
| if [ ! -d android ]; then | |
| npx expo prebuild -p android --clean --no-install --non-interactive | |
| fi | |
| - name: Set up Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| - name: Create Android release signing credentials | |
| run: | | |
| if [ ! -f android/app/eas-build.gradle ]; then | |
| echo "ERROR: android/app/eas-build.gradle was not generated by Expo prebuild." >&2 | |
| exit 1 | |
| fi | |
| keytool -genkeypair \ | |
| -keystore android/app/ci-release-key.keystore \ | |
| -storepass android \ | |
| -alias ci-release-key \ | |
| -keypass android \ | |
| -keyalg RSA \ | |
| -keysize 2048 \ | |
| -validity 10000 \ | |
| -dname "CN=ConUMap CI, OU=CI, O=ConUMap, L=Montreal, S=QC, C=CA" | |
| cat <<'EOF' > credentials.json | |
| { | |
| "android": { | |
| "keystore": { | |
| "keystorePath": "android/app/ci-release-key.keystore", | |
| "keystorePassword": "android", | |
| "keyAlias": "ci-release-key", | |
| "keyPassword": "android" | |
| } | |
| } | |
| } | |
| EOF | |
| - name: Install Maestro CLI | |
| run: | | |
| curl --proto '=https' --tlsv1.2 -fsSL "https://get.maestro.mobile.dev" | bash | |
| echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" | |
| export PATH="$HOME/.maestro/bin:$PATH" | |
| "$HOME/.maestro/bin/maestro" --version | |
| - name: Set up bartek-scripts | |
| run: | | |
| git clone --depth 1 https://github.com/mobile-dev-inc/bartek-scripts.git "$HOME/scripts" | |
| echo "$HOME/scripts/bin" >> "$GITHUB_PATH" | |
| - name: Set up Android command-line tools | |
| run: | | |
| export PATH="$HOME/scripts/bin:$PATH" | |
| install_android_sdk https://dl.google.com/android/repository/commandlinetools-linux-12266719_latest.zip | |
| echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH" | |
| echo "$ANDROID_HOME/emulator" >> "$GITHUB_PATH" | |
| echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH" | |
| - name: Set up Android SDK components | |
| run: | | |
| export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH" | |
| yes | sdkmanager --licenses | |
| sdkmanager --install \ | |
| "emulator" \ | |
| "platform-tools" \ | |
| "$ANDROID_PLATFORM" \ | |
| "build-tools;36.0.0" \ | |
| "$ANDROID_OS_IMAGE" | |
| - name: Create local.properties | |
| run: | | |
| echo "sdk.dir=$ANDROID_HOME" > android/local.properties | |
| cat android/local.properties | |
| - name: Build Android release APK | |
| working-directory: android | |
| run: | | |
| chmod +x ./gradlew | |
| ./gradlew assembleRelease \ | |
| -PreactNativeArchitectures=x86_64 \ | |
| --stacktrace \ | |
| --console=plain \ | |
| --no-daemon \ | |
| --max-workers=2 | |
| - name: Verify APK outputs | |
| run: | | |
| ls -lah android/app/build/outputs/apk/release || true | |
| find android/app/build/outputs/apk/release -type f -name "*.apk" -print | |
| - name: Create AVD | |
| run: | | |
| export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH" | |
| mkdir -p "$ANDROID_AVD_HOME" | |
| echo "no" | avdmanager create avd \ | |
| --force \ | |
| --package "$ANDROID_OS_IMAGE" \ | |
| --name "MyAVD" \ | |
| --sdcard 512M | |
| cat << EOF >> "$ANDROID_AVD_HOME/MyAVD.avd/config.ini" | |
| hw.cpu.ncore=2 | |
| hw.gpu.enabled=yes | |
| hw.gpu.mode=swiftshader_indirect | |
| hw.ramSize=3072 | |
| vm.heapSize=576 | |
| disk.dataPartition.size=2048M | |
| hw.lcd.density=440 | |
| hw.lcd.height=2220 | |
| hw.lcd.width=1080 | |
| fastboot.forceColdBoot=yes | |
| showDeviceFrame=no | |
| EOF | |
| - name: Run AVD | |
| run: | | |
| mkdir -p .maestro/output .maestro/debug .maestro/artifacts .maestro/recordings | |
| export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH" | |
| emulator @MyAVD \ | |
| -verbose \ | |
| -no-snapshot \ | |
| -no-window \ | |
| -no-audio \ | |
| -no-boot-anim \ | |
| -accel on \ | |
| -camera-back none \ | |
| -partition-size 2048 \ | |
| -qemu -m 3072 \ | |
| > .maestro/output/emulator-stdout.log \ | |
| 2> .maestro/output/emulator-stderr.log & | |
| - name: Wait for AVD to start up | |
| run: | | |
| export PATH="$ANDROID_HOME/platform-tools:$PATH" | |
| found="" | |
| for i in {1..120}; do | |
| adb start-server || true | |
| adb devices || true | |
| if adb devices | grep -q "emulator-.*device"; then | |
| found=1 | |
| echo "Emulator device online" | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if [ -z "$found" ]; then | |
| echo "Emulator never became available" | |
| echo "=== emulator stdout ===" | |
| tail -200 .maestro/output/emulator-stdout.log || true | |
| echo "=== emulator stderr ===" | |
| tail -200 .maestro/output/emulator-stderr.log || true | |
| exit 1 | |
| fi | |
| DEVICE_SERIAL="$(adb devices | awk '/emulator-.*device$/ { print $1; exit }')" | |
| if [ -z "$DEVICE_SERIAL" ]; then | |
| echo "Failed to resolve Android emulator serial" >&2 | |
| adb devices | |
| exit 1 | |
| fi | |
| adb -s "$DEVICE_SERIAL" wait-for-device | |
| echo "ANDROID_DEVICE_SERIAL=$DEVICE_SERIAL" >> "$GITHUB_ENV" | |
| adb -s "$DEVICE_SERIAL" shell 'while [[ "$(getprop sys.boot_completed | tr -d "\r")" != "1" ]]; do sleep 1; done;' && echo "Emulator booted" | |
| while true; do | |
| adb -s "$DEVICE_SERIAL" shell service list | grep 'package' && echo 'service "package" is active!' && break | |
| echo 'waiting for service "package" to start' | |
| sleep 1 | |
| done | |
| adb -s "$DEVICE_SERIAL" shell settings put global window_animation_scale 0 || true | |
| adb -s "$DEVICE_SERIAL" shell settings put global transition_animation_scale 0 || true | |
| adb -s "$DEVICE_SERIAL" shell settings put global animator_duration_scale 0 || true | |
| - name: Install app | |
| run: | | |
| APK_PATH="$(find android/app/build/outputs/apk/release -type f -name '*release*.apk' ! -name '*unsigned*' | head -1)" | |
| if [ -z "$APK_PATH" ]; then | |
| echo "Signed release APK not found" >&2 | |
| find android/app/build/outputs/apk/release -type f -name "*.apk" -print || true | |
| exit 1 | |
| fi | |
| adb -s "$ANDROID_DEVICE_SERIAL" install -r "$APK_PATH" | |
| - name: Verify installed app package | |
| run: | | |
| adb -s "$ANDROID_DEVICE_SERIAL" shell pm list packages | grep "$APP_ID" | |
| - name: Run Maestro flow | |
| timeout-minutes: 20 | |
| run: | | |
| export PATH="$HOME/.maestro/bin:$PATH" | |
| export MAESTRO_DRIVER_STARTUP_TIMEOUT=600000 | |
| mkdir -p .maestro/output .maestro/debug .maestro/artifacts .maestro/recordings | |
| VIDEO_FILE=".maestro/recordings/maestro-android-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.mp4" | |
| adb -s "$ANDROID_DEVICE_SERIAL" logcat -c || true | |
| adb -s "$ANDROID_DEVICE_SERIAL" shell screenrecord /sdcard/maestro-android.mp4 >/tmp/android-screenrecord.log 2>&1 & | |
| REC_PID=$! | |
| set +e | |
| maestro --verbose --device "$ANDROID_DEVICE_SERIAL" test .maestro/automated/suite.yaml \ | |
| --format html-detailed \ | |
| --output .maestro/output/report.html \ | |
| --debug-output .maestro/debug \ | |
| --test-output-dir .maestro/artifacts \ | |
| 2>&1 | tee .maestro/output/maestro-console.log | |
| FLOW_EXIT=${PIPESTATUS[0]} | |
| set -e | |
| if kill -0 "$REC_PID" 2>/dev/null; then | |
| kill -INT "$REC_PID" 2>/dev/null || true | |
| wait "$REC_PID" 2>/dev/null || true | |
| fi | |
| sleep 5 | |
| adb -s "$ANDROID_DEVICE_SERIAL" logcat -d > .maestro/output/logcat.txt || true | |
| adb -s "$ANDROID_DEVICE_SERIAL" shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp' > .maestro/output/window-focus.txt || true | |
| adb -s "$ANDROID_DEVICE_SERIAL" shell dumpsys activity activities | head -400 > .maestro/output/activity-dumpsys.txt || true | |
| adb -s "$ANDROID_DEVICE_SERIAL" shell dumpsys package "$APP_ID" | head -250 > .maestro/output/package-dumpsys.txt || true | |
| adb -s "$ANDROID_DEVICE_SERIAL" pull /sdcard/maestro-android.mp4 "$VIDEO_FILE" || true | |
| exit $FLOW_EXIT | |
| - name: Upload screen recording (mp4) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: maestro-android-recording | |
| path: .maestro/recordings/*.mp4 | |
| if-no-files-found: ignore | |
| - name: Upload Maestro outputs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: maestro-android-outputs | |
| path: | | |
| .maestro/output/** | |
| .maestro/debug/** | |
| .maestro/artifacts/** | |
| .maestro/recordings/*.mp4 | |
| if-no-files-found: ignore | |
| include-hidden-files: true |