ci(android-kotlin): add BrowserStack integration tests #59
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 Kotlin BrowserStack | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'android-kotlin/**' | |
| - '.github/workflows/android-kotlin-browserstack.yml' | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'android-kotlin/**' | |
| - '.github/workflows/android-kotlin-browserstack.yml' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-test: | |
| name: Build and Test on BrowserStack | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| # Note: We don't create .env file in CI for security reasons | |
| # Environment variables are passed directly to the build process | |
| - name: Insert test document into Ditto Cloud | |
| run: | | |
| # Use GitHub run ID to create deterministic document ID | |
| DOC_ID="github_test_android_${GITHUB_RUN_ID}_${GITHUB_RUN_NUMBER}" | |
| TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| echo "Creating test document with ID: ${DOC_ID}" | |
| # Insert document using curl with Android Kotlin Task structure | |
| 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\": \"GitHub Test Task Android ${GITHUB_RUN_ID}\", | |
| \"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" | head -n-1) | |
| # 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 "GITHUB_TEST_DOC_ID=${DOC_ID}" >> $GITHUB_ENV | |
| else | |
| echo "❌ Failed to insert document. HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| exit 1 | |
| fi | |
| - 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: Build APK | |
| 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 assembleDebug assembleDebugAndroidTest | |
| echo "APK built successfully" | |
| - 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 APKs to BrowserStack | |
| id: upload | |
| run: | | |
| CREDS="${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" | |
| # Upload main APK | |
| echo "Uploading main 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-${GITHUB_RUN_NUMBER}") | |
| APP_URL=$(echo "$APP_UPLOAD_RESPONSE" | jq -r .app_url) | |
| echo "App upload response: $APP_UPLOAD_RESPONSE" | |
| if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then | |
| echo "❌ Failed to upload main APK" | |
| echo "Response: $APP_UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| # Upload test APK | |
| echo "Uploading test 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-${GITHUB_RUN_NUMBER}") | |
| TEST_URL=$(echo "$TEST_UPLOAD_RESPONSE" | jq -r .test_suite_url) | |
| echo "Test upload response: $TEST_UPLOAD_RESPONSE" | |
| if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then | |
| echo "❌ Failed to upload test APK" | |
| echo "Response: $TEST_UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" | |
| echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" | |
| echo "✓ Successfully uploaded both APKs" | |
| - name: Execute tests on BrowserStack | |
| id: test | |
| run: | | |
| APP_URL="${{ steps.upload.outputs.app_url }}" | |
| TEST_URL="${{ steps.upload.outputs.test_url }}" | |
| echo "App URL: $APP_URL" | |
| echo "Test URL: $TEST_URL" | |
| # Create test execution request | |
| 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\" | |
| ], | |
| \"project\": \"Ditto Android Kotlin Integration\", | |
| \"buildName\": \"Build #${{ github.run_number }} - Sync Test\", | |
| \"buildTag\": \"${{ github.ref_name }}\", | |
| \"deviceLogs\": true, | |
| \"video\": true, | |
| \"networkLogs\": true, | |
| \"autoGrantPermissions\": true, | |
| \"class\": \"live.ditto.quickstart.tasks.SimpleIntegrationTest\", | |
| \"env\": { | |
| \"GITHUB_TEST_DOC_ID\": \"${{ env.GITHUB_TEST_DOC_ID }}\" | |
| } | |
| }") | |
| echo "BrowserStack build response:" | |
| echo "$BUILD_RESPONSE" | jq . | |
| BUILD_ID=$(echo "$BUILD_RESPONSE" | jq -r .build_id) | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "❌ Failed to create BrowserStack build" | |
| 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 }}" | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "❌ No valid BUILD_ID available" | |
| exit 1 | |
| fi | |
| MAX_WAIT_TIME=900 # 15 minutes | |
| CHECK_INTERVAL=30 # Check every 30 seconds | |
| ELAPSED=0 | |
| echo "⏳ Waiting for tests to complete (max ${MAX_WAIT_TIME}s)..." | |
| 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) | |
| if [ "$BUILD_STATUS" = "null" ] || [ -z "$BUILD_STATUS" ]; then | |
| echo "⚠️ Error getting build status, retrying..." | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| continue | |
| fi | |
| echo "📱 Build status: $BUILD_STATUS (${ELAPSED}s elapsed)" | |
| # Check for completion states | |
| if [ "$BUILD_STATUS" = "passed" ] || [ "$BUILD_STATUS" = "failed" ] || [ "$BUILD_STATUS" = "error" ] || [ "$BUILD_STATUS" = "done" ]; then | |
| echo "✅ Build completed with status: $BUILD_STATUS" | |
| break | |
| fi | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| done | |
| if [ $ELAPSED -ge $MAX_WAIT_TIME ]; then | |
| echo "⏰ Tests timed out after ${MAX_WAIT_TIME} seconds" | |
| exit 1 | |
| fi | |
| # 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 . | |
| # Analyze results | |
| BUILD_STATUS=$(echo "$FINAL_RESULT" | jq -r .status) | |
| if [ "$BUILD_STATUS" != "passed" ]; then | |
| echo "❌ Tests failed with status: $BUILD_STATUS" | |
| # Show device-specific failures | |
| echo "$FINAL_RESULT" | jq -r '.devices[]? | select(.sessions[]?.status != "passed") | "❌ Failed on: " + .device' | |
| exit 1 | |
| else | |
| echo "🎉 All tests passed successfully!" | |
| fi | |
| - name: Generate test report | |
| if: always() | |
| run: | | |
| BUILD_ID="${{ steps.test.outputs.build_id }}" | |
| echo "# 📱 BrowserStack Android Kotlin Test Report" > test-report.md | |
| echo "" >> test-report.md | |
| echo "**GitHub Run:** [${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> test-report.md | |
| echo "**Test Document ID:** \`${{ env.GITHUB_TEST_DOC_ID }}\`" >> test-report.md | |
| echo "" >> test-report.md | |
| if [ "$BUILD_ID" != "null" ] && [ -n "$BUILD_ID" ]; then | |
| echo "**BrowserStack Build:** [$BUILD_ID](https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID)" >> test-report.md | |
| echo "" >> test-report.md | |
| # Get detailed results | |
| RESULTS=$(curl -s -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| "https://api-cloud.browserstack.com/app-automate/espresso/v2/builds/$BUILD_ID") | |
| echo "## 📱 Device Results" >> test-report.md | |
| if echo "$RESULTS" | jq -e .devices > /dev/null 2>&1; then | |
| echo "$RESULTS" | jq -r '.devices[]? | "- **" + .device + ":** " + (.sessions[]?.status // "unknown")' >> test-report.md | |
| else | |
| echo "- Unable to retrieve device results" >> test-report.md | |
| fi | |
| else | |
| echo "**Status:** ❌ Build creation failed" >> test-report.md | |
| echo "" >> test-report.md | |
| echo "## ❌ Error" >> test-report.md | |
| echo "Failed to create BrowserStack build. Check workflow logs for details." >> test-report.md | |
| fi | |
| - name: Upload test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-kotlin-browserstack-results | |
| path: | | |
| android-kotlin/QuickStartTasks/app/build/outputs/apk/ | |
| android-kotlin/QuickStartTasks/app/build/reports/ | |
| test-report.md | |
| - name: Comment PR with results | |
| if: github.event_name == 'pull_request' && always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const buildId = '${{ steps.test.outputs.build_id }}'; | |
| const status = '${{ job.status }}'; | |
| const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; | |
| const testDocId = '${{ env.GITHUB_TEST_DOC_ID }}'; | |
| let reportContent = ''; | |
| try { | |
| reportContent = fs.readFileSync('test-report.md', 'utf8'); | |
| } catch (error) { | |
| reportContent = '📱 **Android Kotlin BrowserStack Test Report**\n\n❌ Failed to generate detailed report.'; | |
| } | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: reportContent | |
| }); |