ci(android): add pipelines with BrowserStack integration #52
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 Tests | |
| 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 | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Android SDK | |
| uses: ./.github/actions/android-sdk-setup | |
| - name: Setup Ditto Environment | |
| uses: ./.github/actions/ditto-env-setup | |
| with: | |
| use-secrets: 'true' | |
| 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 }} | |
| - name: Cache Gradle | |
| uses: ./.github/actions/gradle-cache | |
| # Step 1: Lint | |
| - name: Lint Code | |
| working-directory: android-kotlin/QuickStartTasks | |
| run: | | |
| ./gradlew lintDebug | |
| echo "Lint completed successfully" | |
| # Step 2: Build - APKs bundle | |
| - name: Build APK | |
| working-directory: android-kotlin/QuickStartTasks | |
| run: | | |
| ./gradlew assembleDebug assembleDebugAndroidTest | |
| echo "APK built successfully" | |
| # Step 3: Seed - HTTP POST document to Ditto Cloud | |
| - name: Insert test document into Ditto Cloud | |
| run: | | |
| # Use GitHub run ID to create deterministic document ID | |
| DOC_ID="github_android-kotlin_${GITHUB_RUN_ID}_${GITHUB_RUN_NUMBER}" | |
| TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| # Insert document using curl with correct JSON 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 ${GITHUB_RUN_ID} - Android Kotlin\", | |
| \"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 | |
| # Step 4: Upload - app and tests to BrowserStack | |
| - name: Upload APKs to BrowserStack | |
| id: upload | |
| run: | | |
| # Upload app APK | |
| echo "Uploading app APK..." | |
| APP_UPLOAD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ | |
| -F "file=@android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk" \ | |
| -F "custom_id=ditto-android-kotlin-app-${{ github.run_id }}") | |
| echo "App upload response: $APP_UPLOAD_RESPONSE" | |
| APP_URL=$(echo $APP_UPLOAD_RESPONSE | jq -r .app_url) | |
| if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then | |
| echo "Error: Failed to upload app APK" | |
| echo "Response: $APP_UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "app_url=$APP_URL" >> $GITHUB_OUTPUT | |
| echo "App uploaded successfully: $APP_URL" | |
| # Upload test APK | |
| echo "Uploading test APK..." | |
| TEST_UPLOAD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/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_id }}") | |
| echo "Test upload response: $TEST_UPLOAD_RESPONSE" | |
| TEST_URL=$(echo $TEST_UPLOAD_RESPONSE | jq -r .test_url) | |
| if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then | |
| echo "Error: Failed to upload test APK" | |
| echo "Response: $TEST_UPLOAD_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "test_url=$TEST_URL" >> $GITHUB_OUTPUT | |
| echo "Test APK uploaded successfully: $TEST_URL" | |
| # Step 5: Test - tests wait for seeded document to appear | |
| - name: Execute tests on BrowserStack | |
| id: test | |
| run: | | |
| # Validate inputs | |
| 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 diverse device configurations | |
| 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\", | |
| \"Samsung Galaxy S22-12.0\" | |
| ], | |
| \"project\": \"Ditto Quickstart - Android Kotlin\", | |
| \"buildName\": \"Build #${{ github.run_number }}\", | |
| \"buildTag\": \"${{ github.ref_name }}\", | |
| \"deviceLogs\": true, | |
| \"video\": true, | |
| \"networkLogs\": true, | |
| \"autoGrantPermissions\": true, | |
| \"testAnnotations\": { | |
| \"data\": { | |
| \"github_run_id\": \"${{ github.run_id }}\", | |
| \"github_test_doc_id\": \"${{ env.GITHUB_TEST_DOC_ID }}\", | |
| \"project_type\": \"android-kotlin\" | |
| } | |
| }, | |
| \"instrumentationLogs\": true, | |
| \"testRunnerClass\": \"androidx.test.runner.AndroidJUnitRunner\", | |
| \"testRunnerArgs\": { | |
| \"github_run_id\": \"${{ github.run_id }}\", | |
| \"github_test_doc_id\": \"${{ env.GITHUB_TEST_DOC_ID }}\" | |
| } | |
| }") | |
| echo "BrowserStack API Response:" | |
| echo "$BUILD_RESPONSE" | |
| BUILD_ID=$(echo "$BUILD_RESPONSE" | jq -r .build_id) | |
| 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" | |
| # Step 6: Wait - poll for results | |
| - name: Wait for BrowserStack tests to complete | |
| run: | | |
| BUILD_ID="${{ steps.test.outputs.build_id }}" | |
| 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) | |
| 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)" | |
| 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 . | |
| if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then | |
| BUILD_STATUS=$(echo "$FINAL_RESULT" | jq -r .status) | |
| if [ "$BUILD_STATUS" != "passed" ]; then | |
| echo "Build failed with status: $BUILD_STATUS" | |
| 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 | |
| - 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 | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "Build ID: N/A (Build creation failed)" >> test-report.md | |
| echo "" >> test-report.md | |
| echo "## Error" >> test-report.md | |
| echo "Failed to create BrowserStack build. Check the 'Execute tests on BrowserStack' step for details." >> test-report.md | |
| else | |
| echo "Build ID: $BUILD_ID" >> test-report.md | |
| echo "View full report: 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 | |
| echo "### Tested Devices:" >> test-report.md | |
| if echo "$RESULTS" | jq -e .devices > /dev/null 2>&1; then | |
| echo "$RESULTS" | jq -r '.devices[] | "- \(.device): \(.sessions[0].status // "unknown")"' >> test-report.md | |
| else | |
| echo "Unable to retrieve device results" >> test-report.md | |
| fi | |
| echo "" >> test-report.md | |
| echo "## Test Document" >> test-report.md | |
| echo "GitHub Test Document ID: ${{ env.GITHUB_TEST_DOC_ID }}" >> 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 buildId = '${{ steps.test.outputs.build_id }}'; | |
| const status = '${{ job.status }}'; | |
| const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; | |
| let body; | |
| if (buildId === 'null' || buildId === '' || !buildId) { | |
| body = `## 📱 BrowserStack Android Kotlin Test Results | |
| **Status:** ❌ Failed (Build creation failed) | |
| **Build:** [#${{ github.run_number }}](${runUrl}) | |
| **Issue:** Failed to create BrowserStack build. Check the workflow logs for details. | |
| ### Expected Devices: | |
| - Google Pixel 8 (Android 14) | |
| - Samsung Galaxy S23 (Android 13) | |
| - Google Pixel 6 (Android 12) | |
| - OnePlus 9 (Android 11) | |
| - Samsung Galaxy S22 (Android 12) | |
| `; | |
| } else { | |
| const bsUrl = `https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`; | |
| body = `## 📱 BrowserStack Android Kotlin Test Results | |
| **Status:** ${status === 'success' ? '✅ Passed' : '❌ Failed'} | |
| **Build:** [#${{ github.run_number }}](${runUrl}) | |
| **BrowserStack:** [View detailed results](${bsUrl}) | |
| **Test Document:** ${{ env.GITHUB_TEST_DOC_ID }} | |
| ### Tested Devices: | |
| - Google Pixel 8 (Android 14) | |
| - Samsung Galaxy S23 (Android 13) | |
| - Google Pixel 6 (Android 12) | |
| - OnePlus 9 (Android 11) | |
| - Samsung Galaxy S22 (Android 12) | |
| `; | |
| } | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); |