ci(android): add pipelines with BrowserStack integration #40
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: Insert test document into Ditto Cloud | |
| uses: ./.github/actions/ditto-test-document-insert | |
| with: | |
| project-type: android-kotlin | |
| ditto-api-key: ${{ secrets.DITTO_API_KEY }} | |
| ditto-api-url: ${{ secrets.DITTO_API_URL }} | |
| - name: Cache Gradle | |
| uses: ./.github/actions/gradle-cache | |
| - name: Build APK | |
| working-directory: android-kotlin/QuickStartTasks | |
| run: | | |
| ./gradlew assembleDebug assembleDebugAndroidTest | |
| echo "APK built successfully" | |
| - 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" | |
| - 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\" | |
| ], | |
| \"projectName\": \"Ditto 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" | |
| - 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 | |
| }); |