feat: upgrade react-native quickstart to 0.82.1 with ditto 4.13.0 #361
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: React Native CI | |
| on: | |
| pull_request: | |
| paths: | |
| - "react-native/**" | |
| - ".github/workflows/react-native-ci.yml" | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| cache-dependency-path: react-native/yarn.lock | |
| - name: Install dependencies | |
| working-directory: react-native | |
| run: yarn install --frozen-lockfile | |
| - name: Run linting | |
| working-directory: react-native | |
| run: yarn lint | |
| build-android: | |
| name: Build Android | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| cache-dependency-path: react-native/yarn.lock | |
| - name: Install dependencies | |
| working-directory: react-native | |
| run: yarn install --frozen-lockfile | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "17" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Create .env file (production credentials for builds) | |
| run: | | |
| echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env | |
| echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env | |
| echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env | |
| - name: Cache Gradle dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| react-native/android/.gradle | |
| key: gradle-${{ runner.os }}-${{ hashFiles('react-native/android/gradle/wrapper/gradle-wrapper.properties', 'react-native/android/**/*.gradle*') }} | |
| restore-keys: | | |
| gradle-${{ runner.os }}- | |
| - name: Build Android APK | |
| working-directory: react-native/android | |
| # Use direct gradlew instead of yarn android to generate unsigned APK required by BrowserStack | |
| # yarn android creates signed builds that don't work with BrowserStack real device testing | |
| run: NODE_ENV=production ./gradlew assembleRelease --no-daemon --console=plain | |
| - name: Upload Android APK artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apk-${{ github.run_number }} | |
| path: react-native/android/app/build/outputs/apk/release/app-release.apk | |
| retention-days: 1 | |
| build-ios: | |
| name: Build iOS | |
| runs-on: macos-latest | |
| needs: lint | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| cache-dependency-path: react-native/yarn.lock | |
| - name: Install dependencies | |
| working-directory: react-native | |
| run: yarn install --frozen-lockfile | |
| - name: Create .env file (production credentials for builds) | |
| run: | | |
| echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env | |
| echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env | |
| echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env | |
| - name: Cache CocoaPods dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| react-native/ios/Pods | |
| ~/Library/Caches/CocoaPods | |
| ~/.cocoapods | |
| key: cocoapods-${{ runner.os }}-${{ hashFiles('react-native/ios/Podfile.lock') }} | |
| restore-keys: | | |
| cocoapods-${{ runner.os }}- | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: "16.4" | |
| - name: Setup Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.3' | |
| bundler-cache: true | |
| working-directory: react-native | |
| - name: Install Ruby gems | |
| working-directory: react-native | |
| run: bundle install | |
| - name: Install CocoaPods | |
| working-directory: react-native/ios | |
| run: bundle exec pod install | |
| - name: Build iOS Archive and IPA (for BrowserStack) | |
| working-directory: react-native | |
| run: | | |
| echo "🍎 Building iOS device .ipa for BrowserStack..." | |
| # Use direct xcodebuild instead of yarn ios to generate unsigned IPA required by BrowserStack | |
| # yarn ios creates signed builds for simulators that don't work with BrowserStack real device testing | |
| # Build and archive iOS app for real device | |
| xcodebuild -workspace ios/DittoReactNativeSampleApp.xcworkspace \ | |
| -scheme DittoReactNativeSampleApp \ | |
| -configuration Release \ | |
| -destination 'generic/platform=iOS' \ | |
| -archivePath ios/build/DittoReactNativeSampleApp.xcarchive \ | |
| archive \ | |
| CODE_SIGN_IDENTITY="" \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGNING_ALLOWED=NO | |
| echo "📦 Creating unsigned .ipa for BrowserStack..." | |
| # Find the .app bundle from the archive | |
| APP_BUNDLE_PATH=$(find ios/build/DittoReactNativeSampleApp.xcarchive/Products/Applications -maxdepth 1 -name "*.app" -type d | head -1) | |
| if [ -d "$APP_BUNDLE_PATH" ]; then | |
| echo "✅ iOS app bundle found: $APP_BUNDLE_PATH" | |
| # Create unsigned IPA: Payload/<App>.app zipped as .ipa | |
| mkdir -p ios/build/Payload | |
| cp -R "$APP_BUNDLE_PATH" ios/build/Payload/ | |
| (cd ios/build && zip -qry DittoReactNativeSampleApp-unsigned.ipa Payload && rm -rf Payload) | |
| if [ -f "ios/build/DittoReactNativeSampleApp-unsigned.ipa" ]; then | |
| echo "✅ Unsigned .ipa created successfully" | |
| ls -la ios/build/DittoReactNativeSampleApp-unsigned.ipa | |
| else | |
| echo "❌ Failed to create .ipa file" | |
| exit 1 | |
| fi | |
| else | |
| echo "❌ iOS app bundle not found in archive" | |
| exit 1 | |
| fi | |
| - name: Upload iOS IPA artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-ipa-${{ github.run_number }} | |
| path: react-native/ios/build/DittoReactNativeSampleApp-unsigned.ipa | |
| retention-days: 1 | |
| browserstack-android: | |
| name: Test Android - BrowserStack Maestro | |
| runs-on: ubuntu-latest | |
| needs: [build-android] | |
| timeout-minutes: 150 | |
| outputs: | |
| build_id: ${{ steps.execute-tests.outputs.build_id }} | |
| status: ${{ job.status }} | |
| # TODO: BrowserStack Android Maestro API environment variables don't work (confirmed by BS staff) | |
| # On their roadmap with estimate: Oct 2025. Will revert to setEnvVariables when ready. | |
| # Using sed workaround to replace ${MAESTRO_DITTO_CLOUD_TASK_TITLE} with actual task name in test files | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download Android APK | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-apk-${{ github.run_number }} | |
| path: ./artifacts/ | |
| - name: Get BrowserStack build info (Android) | |
| id: build-info-android | |
| uses: ./.github/actions/generate-browserstack-names | |
| with: | |
| platform-suffix: ' (Android)' | |
| title-max-length: '90' | |
| commit-max-length: '130' | |
| - name: Seed, create test suite, upload, and execute Android tests on BrowserStack | |
| id: execute-tests | |
| uses: nick-fields/retry@v3 | |
| with: | |
| max_attempts: 5 | |
| timeout_minutes: 20 | |
| retry_wait_seconds: 900 | |
| command: | | |
| # Seed test task to Ditto Cloud | |
| echo "Seeding test task to Ditto Cloud..." | |
| TIMESTAMP=$(date +%s) | |
| INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) | |
| SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H 'Content-type: application/json' \ | |
| -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ | |
| -d "{ | |
| \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", | |
| \"args\": { | |
| \"newTask\": { | |
| \"_id\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"title\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"done\": false, | |
| \"deleted\": false | |
| } | |
| } | |
| }" \ | |
| "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") | |
| HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) | |
| BODY=$(echo "$SEED_RESPONSE" | sed '$d') | |
| if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then | |
| TASK_TITLE="${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}" | |
| echo "Seeded task: $TASK_TITLE" | |
| else | |
| echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| exit 1 | |
| fi | |
| # Upload APK to BrowserStack | |
| echo "Uploading APK to BrowserStack..." | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ | |
| -F "file=@./artifacts/app-release.apk" \ | |
| -F "custom_id=ReactNative-Android-${{ github.run_id }}") | |
| APP_URL=$(echo $response | yq eval -p=json '.app_url') | |
| if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then | |
| echo "Error: Failed to upload APK" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "APK uploaded: $APP_URL" | |
| # Create Maestro test suite ZIP with seeded task title | |
| cd react-native | |
| rm -rf maestro-tests | |
| mkdir maestro-tests | |
| # Replace MAESTRO_DITTO_CLOUD_TASK_TITLE with actual seeded task name | |
| sed "s/\${MAESTRO_DITTO_CLOUD_TASK_TITLE}/$TASK_TITLE/g" \ | |
| .maestro/flows/01-app-launch-and-seeded-tasks-android.yaml > maestro-tests/01-app-launch-and-seeded-tasks-android.yaml | |
| mkdir maestro-tests/flows | |
| cp maestro-tests/01-app-launch-and-seeded-tasks-android.yaml maestro-tests/flows/ | |
| zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" | |
| cd .. | |
| # Upload Maestro test suite to BrowserStack | |
| echo "Uploading test suite to BrowserStack..." | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ | |
| -F "file=@react-native/maestro-tests.zip" \ | |
| -F "custom_id=ReactNative-Tests-${{ github.run_id }}") | |
| TEST_SUITE_URL=$(echo "$response" | yq eval -p=json '.test_suite_url') | |
| if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then | |
| echo "Error: Failed to upload test suite" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "Test suite uploaded: $TEST_SUITE_URL" | |
| # Execute tests | |
| # Load devices from centralized config | |
| DEVICES=$(yq eval -o=json -I=0 '.["react-native"].android.devices' .github/browserstack-devices.yml) | |
| echo "Loaded devices: $DEVICES" | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/android/build" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"app\": \"$APP_URL\", | |
| \"testSuite\": \"$TEST_SUITE_URL\", | |
| \"project\": \"${{ steps.build-info-android.outputs.project-name }}\", | |
| \"buildName\": \"${{ steps.build-info-android.outputs.build-name }}\", | |
| \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", | |
| \"devices\": $DEVICES, | |
| \"execute\": [\"01-app-launch-and-seeded-tasks-android.yaml\"], | |
| \"deviceLogs\": true, | |
| \"networkLogs\": true, | |
| \"video\": true | |
| }") | |
| echo "Execution response: $response" | |
| build_id=$(echo $response | yq eval '.build_id') | |
| if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then | |
| echo "❌ Failed to create BrowserStack build" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "BrowserStack Build ID: $build_id" | |
| # Wait for test completion and get results | |
| MAX_WAIT_TIME=1080 # 18 minutes | |
| CHECK_INTERVAL=10 # Check every 10 seconds | |
| ELAPSED=0 | |
| echo "⏳ Waiting for test execution to complete..." | |
| while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do | |
| response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") | |
| status=$(echo $response | yq eval '.status') | |
| echo "Test status: $status (${ELAPSED}s elapsed)" | |
| if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then | |
| echo "Tests completed with status: $status" | |
| echo $response | jq '.' | |
| if [ "$status" = "failed" ]; then | |
| echo "❌ Android Maestro tests failed!" | |
| exit 1 | |
| else | |
| echo "✅ Android Maestro tests passed!" | |
| fi | |
| break | |
| elif [ "$status" = "error" ]; then | |
| echo "❌ BrowserStack reported build error. Fetching session details..." | |
| echo $response | jq '.' | |
| echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do | |
| if [ -n "$sid" ]; then | |
| echo "Session $sid details:" | |
| curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' | |
| fi | |
| done | |
| exit 1 | |
| fi | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| done | |
| if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then | |
| echo "❌ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" | |
| exit 1 | |
| fi | |
| browserstack-ios: | |
| name: Test iOS - BrowserStack Maestro | |
| runs-on: ubuntu-latest | |
| needs: [build-ios] | |
| timeout-minutes: 150 | |
| outputs: | |
| build_id: ${{ steps.execute-tests.outputs.build_id }} | |
| status: ${{ job.status }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download iOS IPA | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ios-ipa-${{ github.run_number }} | |
| path: ./artifacts/ | |
| - name: Get BrowserStack build info (iOS) | |
| id: build-info-ios | |
| uses: ./.github/actions/generate-browserstack-names | |
| with: | |
| platform-suffix: ' (iOS)' | |
| title-max-length: '90' | |
| commit-max-length: '130' | |
| - name: Seed, upload, and execute iOS Maestro tests on BrowserStack | |
| id: execute-tests | |
| uses: nick-fields/retry@v3 | |
| with: | |
| max_attempts: 5 | |
| timeout_minutes: 20 | |
| retry_wait_seconds: 900 | |
| command: | | |
| # Seed test task to Ditto Cloud | |
| echo "Seeding test task to Ditto Cloud..." | |
| TIMESTAMP=$(date +%s) | |
| INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) | |
| SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H 'Content-type: application/json' \ | |
| -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ | |
| -d "{ | |
| \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", | |
| \"args\": { | |
| \"newTask\": { | |
| \"_id\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"title\": \"${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"done\": false, | |
| \"deleted\": false | |
| } | |
| } | |
| }" \ | |
| "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") | |
| HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) | |
| BODY=$(echo "$SEED_RESPONSE" | sed '$d') | |
| if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then | |
| TASK_TITLE="${INVERTED_TIMESTAMP}_react-native_ci_test_${{ github.run_id }}_${{ github.run_number }}" | |
| echo "Seeded task: $TASK_TITLE" | |
| else | |
| echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| exit 1 | |
| fi | |
| # Upload IPA to BrowserStack | |
| echo "Uploading IPA to BrowserStack..." | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/app" \ | |
| -F "file=@./artifacts/DittoReactNativeSampleApp-unsigned.ipa" \ | |
| -F "custom_id=ReactNative-iOS-${{ github.run_id }}") | |
| APP_URL=$(echo $response | yq eval -p=json '.app_url') | |
| if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then | |
| echo "Error: Failed to upload IPA" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "IPA uploaded: $APP_URL" | |
| # Create Maestro test suite ZIP | |
| cd react-native | |
| rm -rf maestro-tests | |
| mkdir maestro-tests | |
| # Copy iOS-specific test | |
| cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/ | |
| # Create flows/ subfolder | |
| mkdir maestro-tests/flows | |
| cp .maestro/flows/01-app-launch-and-seeded-tasks-ios.yaml maestro-tests/flows/ | |
| # Create ZIP | |
| zip -r maestro-tests.zip maestro-tests -x "*.DS_Store" | |
| cd .. | |
| # Upload Maestro test suite to BrowserStack | |
| echo "Uploading test suite to BrowserStack..." | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ | |
| -F "file=@react-native/maestro-tests.zip" \ | |
| -F "custom_id=ReactNative-iOS-Tests-${{ github.run_id }}") | |
| TEST_SUITE_URL=$(echo $response | yq eval '.test_suite_url') | |
| if [ -z "$TEST_SUITE_URL" ] || [ "$TEST_SUITE_URL" = "null" ]; then | |
| echo "Error: Failed to upload test suite" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "Test suite uploaded: $TEST_SUITE_URL" | |
| # Execute tests | |
| # Load devices from centralized config | |
| DEVICES=$(yq eval -o=json -I=0 '.["react-native"].ios.devices' .github/browserstack-devices.yml) | |
| echo "Loaded devices: $DEVICES" | |
| response=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/ios/build" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"app\": \"$APP_URL\", | |
| \"testSuite\": \"$TEST_SUITE_URL\", | |
| \"project\": \"${{ steps.build-info-ios.outputs.project-name }}\", | |
| \"buildName\": \"${{ steps.build-info-ios.outputs.build-name }}\", | |
| \"buildTag\": \"${{ github.head_ref || github.ref_name }}\", | |
| \"devices\": $DEVICES, | |
| \"execute\": [\"01-app-launch-and-seeded-tasks-ios.yaml\"], | |
| \"setEnvVariables\": { | |
| \"MAESTRO_DITTO_CLOUD_TASK_TITLE\": \"$TASK_TITLE\" | |
| }, | |
| \"deviceLogs\": true, | |
| \"networkLogs\": true, | |
| \"video\": true | |
| }") | |
| echo "Execution response: $response" | |
| build_id=$(echo $response | yq eval '.build_id') | |
| if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then | |
| echo "❌ Failed to create BrowserStack build" | |
| echo "Response: $response" | |
| exit 1 | |
| fi | |
| echo "BrowserStack Build ID: $build_id" | |
| # Wait for test completion and get results | |
| MAX_WAIT_TIME=1080 # 18 minutes | |
| CHECK_INTERVAL=10 # Check every 10 seconds | |
| ELAPSED=0 | |
| echo "⏳ Waiting for test execution to complete..." | |
| while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do | |
| response=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id") | |
| status=$(echo $response | yq eval '.status') | |
| echo "Test status: $status (${ELAPSED}s elapsed)" | |
| if [ "$status" = "passed" ] || [ "$status" = "failed" ]; then | |
| echo "Tests completed with status: $status" | |
| echo $response | jq '.' | |
| if [ "$status" = "failed" ]; then | |
| echo "❌ iOS Maestro tests failed!" | |
| exit 1 | |
| else | |
| echo "✅ iOS Maestro tests passed!" | |
| fi | |
| break | |
| elif [ "$status" = "error" ]; then | |
| echo "❌ BrowserStack reported build error. Fetching session details..." | |
| echo $response | jq '.' | |
| echo $response | yq eval '.devices[]?.sessions[]?.id' 2>/dev/null | while read sid; do | |
| if [ -n "$sid" ]; then | |
| echo "Session $sid details:" | |
| curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$build_id/sessions/$sid" | jq '.' | |
| fi | |
| done | |
| exit 1 | |
| fi | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| done | |
| if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then | |
| echo "❌ Tests timed out after ${MAX_WAIT_TIME}s (18 minutes)" | |
| exit 1 | |
| fi | |
| summary: | |
| name: CI Report | |
| runs-on: ubuntu-latest | |
| needs: [browserstack-android, browserstack-ios] | |
| if: always() | |
| steps: | |
| - name: Generate BrowserStack Summary | |
| run: | | |
| echo "## 📱 React Native - BrowserStack Maestro Test Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Android results | |
| echo "### Android Tests" >> $GITHUB_STEP_SUMMARY | |
| ANDROID_BUILD_ID="${{ needs.browserstack-android.outputs.build_id }}" | |
| ANDROID_STATUS="${{ needs.browserstack-android.outputs.status }}" | |
| if [ -n "$ANDROID_BUILD_ID" ] && [ "$ANDROID_BUILD_ID" != "null" ]; then | |
| if [ "$ANDROID_STATUS" = "success" ]; then | |
| echo "✅ **Status:** Passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Status:** Failed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "🔗 **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$ANDROID_BUILD_ID)**" >> $GITHUB_STEP_SUMMARY | |
| echo "Build ID: \`$ANDROID_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # iOS results | |
| echo "### iOS Tests" >> $GITHUB_STEP_SUMMARY | |
| IOS_BUILD_ID="${{ needs.browserstack-ios.outputs.build_id }}" | |
| IOS_STATUS="${{ needs.browserstack-ios.outputs.status }}" | |
| if [ -n "$IOS_BUILD_ID" ] && [ "$IOS_BUILD_ID" != "null" ]; then | |
| if [ "$IOS_STATUS" = "success" ]; then | |
| echo "✅ **Status:** Passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Status:** Failed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "🔗 **[View Dashboard](https://app-automate.browserstack.com/dashboard/v2/builds/$IOS_BUILD_ID)**" >> $GITHUB_STEP_SUMMARY | |
| echo "Build ID: \`$IOS_BUILD_ID\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Status:** Failed (No build created)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Summary | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| if [ "$ANDROID_STATUS" = "success" ] && [ "$IOS_STATUS" = "success" ]; then | |
| echo "✅ **Overall Result:** All tests passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ **Overall Result:** Some tests failed" >> $GITHUB_STEP_SUMMARY | |
| fi |