ci(android-java): add BrowserStack integration tests #84
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: Android Java CI | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - 'android-java/**' | |
| - '.github/workflows/android-java-ci.yml' | |
| pull_request: | |
| branches: [ main ] | |
| paths: | |
| - 'android-java/**' | |
| - '.github/workflows/android-java-ci.yml' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| name: Lint (ubuntu-latest) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Cache Gradle dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| android-kotlin/QuickStartTasks/.gradle | |
| key: gradle-${{ runner.os }}-${{ hashFiles('android-kotlin/QuickStartTasks/gradle/wrapper/gradle-wrapper.properties', 'android-kotlin/QuickStartTasks/**/*.gradle*', 'android-kotlin/QuickStartTasks/gradle/libs.versions.toml') }} | |
| restore-keys: | | |
| gradle-${{ runner.os }}- | |
| - name: Create test .env file | |
| run: | | |
| echo "DITTO_APP_ID=test" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=test" >> .env | |
| echo "DITTO_AUTH_URL=test" >> .env | |
| echo "DITTO_WEBSOCKET_URL=test" >> .env | |
| - name: Run Android linting | |
| working-directory: android-kotlin/QuickStartTasks | |
| run: ./gradlew lint | |
| build-and-test: | |
| name: Build and Test | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 30 | |
| outputs: | |
| test_doc_title: ${{ steps.test_doc.outputs.test_doc_title }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - 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: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Cache Gradle dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| restore-keys: | | |
| ${{ runner.os }}-gradle- | |
| - name: Create test .env file | |
| run: | | |
| echo "DITTO_APP_ID=test" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=test" >> .env | |
| echo "DITTO_AUTH_URL=test" >> .env | |
| echo "DITTO_WEBSOCKET_URL=test" >> .env | |
| - name: Generate test document title | |
| id: test_doc | |
| run: | | |
| # Create a unique GitHub test document with inverted timestamp to appear at top | |
| TIMESTAMP=$(date +%s) | |
| INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) | |
| DOC_ID="${INVERTED_TIMESTAMP}_android_ci_test_${{ github.run_id }}_${{ github.run_number }}" | |
| DOC_TITLE="${INVERTED_TIMESTAMP}_android_ci_test_${{ github.run_id }}_${{ github.run_number }}" | |
| echo "test_doc_id=$DOC_ID" >> $GITHUB_OUTPUT | |
| echo "test_doc_title=$DOC_TITLE" >> $GITHUB_OUTPUT | |
| echo "π Generated test document (inverted timestamp for top position)" | |
| echo "π ID: '${DOC_ID}'" | |
| echo "π Title: '${DOC_TITLE}'" | |
| echo "π Timestamp: ${TIMESTAMP} β Inverted: ${INVERTED_TIMESTAMP}" | |
| - name: Build APKs | |
| working-directory: android-kotlin/QuickStartTasks | |
| env: | |
| DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} | |
| DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} | |
| DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} | |
| DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} | |
| TEST_DOCUMENT_TITLE: ${{ steps.test_doc.outputs.test_doc_title }} | |
| run: ./gradlew assembleDebug assembleDebugAndroidTest | |
| - name: Run unit tests | |
| working-directory: android-kotlin/QuickStartTasks | |
| env: | |
| DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} | |
| DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} | |
| DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} | |
| DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} | |
| run: ./gradlew test | |
| - name: Upload APK artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apks-${{ github.run_number }} | |
| path: | | |
| android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk | |
| android-kotlin/QuickStartTasks/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk | |
| retention-days: 1 | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-reports-${{ github.run_number }} | |
| path: android-kotlin/QuickStartTasks/app/build/reports/ | |
| retention-days: 1 | |
| browserstack-test: | |
| name: BrowserStack Device Testing | |
| runs-on: ubuntu-latest | |
| needs: build-and-test | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download APK artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-apks-${{ github.run_number }} | |
| path: android-kotlin/QuickStartTasks/app/build/outputs/apk/ | |
| - name: Insert test document into Ditto Cloud | |
| run: | | |
| # Use the same document title that was built into the APK | |
| DOC_TITLE="${{ needs.build-and-test.outputs.test_doc_title }}" | |
| DOC_ID="$DOC_TITLE" | |
| echo "π Inserting test document that matches build-time configuration" | |
| echo "π ID: '${DOC_ID}'" | |
| echo "π Title: '${DOC_TITLE}'" | |
| # Insert document using Ditto API v4 (same as Swift workflow) | |
| 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\": \"${DOC_ID}\", | |
| \"title\": \"${DOC_TITLE}\", | |
| \"done\": false, | |
| \"deleted\": false | |
| } | |
| } | |
| }" \ | |
| "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") | |
| # Extract HTTP status code and response body | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -n1) | |
| BODY=$(echo "$RESPONSE" | sed '$d') | |
| # Check if insertion was successful | |
| if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then | |
| echo "β Successfully inserted test document with ID: ${DOC_ID}" | |
| echo "β Document title: ${DOC_TITLE}" | |
| else | |
| echo "β Failed to insert document. HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| exit 1 | |
| fi | |
| - name: Upload APKs to BrowserStack | |
| id: upload | |
| run: | | |
| CREDS="${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" | |
| # 1. Upload AUT (app-debug.apk) | |
| APP_UPLOAD_RESPONSE=$(curl -u "$CREDS" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" \ | |
| -F "file=@android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk" \ | |
| -F "custom_id=ditto-android-kotlin-app") | |
| APP_URL=$(echo "$APP_UPLOAD_RESPONSE" | jq -r .app_url) | |
| echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" | |
| # 2. Upload Espresso test-suite (app-debug-androidTest.apk) | |
| TEST_UPLOAD_RESPONSE=$(curl -u "$CREDS" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" \ | |
| -F "file=@android-kotlin/QuickStartTasks/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \ | |
| -F "custom_id=ditto-android-kotlin-test") | |
| TEST_URL=$(echo "$TEST_UPLOAD_RESPONSE" | jq -r .test_suite_url) | |
| echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" | |
| - name: Execute tests on BrowserStack | |
| id: test | |
| run: | | |
| # Validate inputs before creating test execution request | |
| APP_URL="${{ steps.upload.outputs.app_url }}" | |
| TEST_URL="${{ steps.upload.outputs.test_url }}" | |
| echo "App URL: $APP_URL" | |
| echo "Test URL: $TEST_URL" | |
| if [ -z "$APP_URL" ] || [ "$APP_URL" = "null" ]; then | |
| echo "Error: No valid app URL available" | |
| exit 1 | |
| fi | |
| if [ -z "$TEST_URL" ] || [ "$TEST_URL" = "null" ]; then | |
| echo "Error: No valid test URL available" | |
| exit 1 | |
| fi | |
| # Create test execution request with instrumentationOptions (correct approach for Android) | |
| TITLE="${{ needs.build-and-test.outputs.test_doc_title }}" | |
| BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"app\": \"$APP_URL\", | |
| \"testSuite\": \"$TEST_URL\", | |
| \"devices\": [ | |
| \"Google Pixel 8-14.0\", | |
| \"Samsung Galaxy S23-13.0\", | |
| \"Google Pixel 6-12.0\", | |
| \"OnePlus 9-11.0\" | |
| ], | |
| \"project\": \"Ditto Android Java\", | |
| \"buildName\": \"Build #${{ github.run_number }}\", | |
| \"buildTag\": \"${{ github.ref_name }}\", | |
| \"deviceLogs\": true, | |
| \"video\": true, | |
| \"networkLogs\": true, | |
| \"autoGrantPermissions\": true, | |
| \"instrumentationOptions\": { | |
| \"github_test_doc_id\": \"$TITLE\" | |
| } | |
| }") | |
| echo "BrowserStack API Response:" | |
| echo "$BUILD_RESPONSE" | |
| BUILD_ID=$(echo "$BUILD_RESPONSE" | jq -r .build_id) | |
| # Check if BUILD_ID is null or empty | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "Error: Failed to create BrowserStack build" | |
| echo "Response: $BUILD_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT | |
| echo "Build started with ID: $BUILD_ID" | |
| - name: Wait for BrowserStack tests to complete | |
| run: | | |
| BUILD_ID="${{ steps.test.outputs.build_id }}" | |
| # Validate BUILD_ID before proceeding | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "Error: No valid BUILD_ID available. Skipping test monitoring." | |
| exit 1 | |
| fi | |
| MAX_WAIT_TIME=1800 # 30 minutes | |
| CHECK_INTERVAL=30 # Check every 30 seconds | |
| ELAPSED=0 | |
| while [ $ELAPSED -lt $MAX_WAIT_TIME ]; do | |
| BUILD_STATUS_RESPONSE=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") | |
| BUILD_STATUS=$(echo "$BUILD_STATUS_RESPONSE" | jq -r .status) | |
| # Check for API errors | |
| if [ "$BUILD_STATUS" = "null" ] || [ -z "$BUILD_STATUS" ]; then | |
| echo "Error getting build status. Response: $BUILD_STATUS_RESPONSE" | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| continue | |
| fi | |
| echo "Build status: $BUILD_STATUS (elapsed: ${ELAPSED}s)" | |
| echo "Full response: $BUILD_STATUS_RESPONSE" | |
| # Check for completion states - BrowserStack uses different status values | |
| if [ "$BUILD_STATUS" = "done" ] || [ "$BUILD_STATUS" = "failed" ] || [ "$BUILD_STATUS" = "error" ] || [ "$BUILD_STATUS" = "passed" ] || [ "$BUILD_STATUS" = "completed" ]; then | |
| echo "Build completed with status: $BUILD_STATUS" | |
| break | |
| fi | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| done | |
| # Get final results | |
| FINAL_RESULT=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") | |
| echo "Final build result:" | |
| echo "$FINAL_RESULT" | jq . | |
| # Check if we got valid results | |
| if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then | |
| # Check if the overall build passed | |
| BUILD_STATUS=$(echo "$FINAL_RESULT" | jq -r .status) | |
| if [ "$BUILD_STATUS" != "passed" ]; then | |
| echo "Build failed with status: $BUILD_STATUS" | |
| # Check each device for failures | |
| FAILED_TESTS=$(echo "$FINAL_RESULT" | jq -r '.devices[] | select(.sessions[].status != "passed") | .device') | |
| if [ -n "$FAILED_TESTS" ]; then | |
| echo "Tests failed on devices: $FAILED_TESTS" | |
| fi | |
| exit 1 | |
| else | |
| echo "All tests passed successfully!" | |
| fi | |
| else | |
| echo "Warning: Could not parse final results" | |
| echo "Raw response: $FINAL_RESULT" | |
| fi |