ci(java-spring): add BrowserStack integration tests #107
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-java/.gradle | |
| key: gradle-${{ runner.os }}-${{ hashFiles('android-java/gradle/wrapper/gradle-wrapper.properties', 'android-java/**/*.gradle*') }} | |
| 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-java | |
| run: ./gradlew lint | |
| build: | |
| name: Build APKs | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 20 | |
| 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 .env file | |
| 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: Build APKs | |
| working-directory: android-java | |
| run: ./gradlew assembleDebug assembleDebugAndroidTest | |
| - name: Upload APK artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apks-${{ github.run_number }} | |
| path: | | |
| android-java/app/build/outputs/apk/debug/app-debug.apk | |
| android-java/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk | |
| retention-days: 1 | |
| browserstack-test: | |
| name: BrowserStack Device Testing | |
| runs-on: ubuntu-latest | |
| needs: build | |
| 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-java/app/build/outputs/apk/ | |
| - name: Insert test document into Ditto Cloud | |
| run: | | |
| # Generate test document for BrowserStack testing | |
| 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 "π Inserting test document for BrowserStack testing" | |
| echo "π ID: '${DOC_ID}'" | |
| echo "π Title: '${DOC_TITLE}'" | |
| # Store title for later use in BrowserStack step | |
| echo "TEST_DOC_TITLE=${DOC_TITLE}" >> $GITHUB_ENV | |
| # Insert document using Ditto API v4 (same as Kotlin 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 }}" | |
| # Upload app APK | |
| echo "π± Uploading app APK to BrowserStack..." | |
| APP_RESPONSE=$(curl -u "$CREDS" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" \ | |
| -F "file=@android-java/app/build/outputs/apk/debug/app-debug.apk" \ | |
| -F "custom_id=ditto-android-java-app") | |
| APP_URL=$(echo "$APP_RESPONSE" | jq -r .app_url) | |
| echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" | |
| if [ "$APP_URL" = "null" ] || [ -z "$APP_URL" ]; then | |
| echo "β Failed to upload app APK" | |
| echo "Response: $APP_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "β App APK uploaded: $APP_URL" | |
| # Upload test APK | |
| echo "π§ͺ Uploading test APK to BrowserStack..." | |
| TEST_RESPONSE=$(curl -u "$CREDS" \ | |
| -X POST "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" \ | |
| -F "file=@android-java/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \ | |
| -F "custom_id=ditto-android-java-test") | |
| TEST_URL=$(echo "$TEST_RESPONSE" | jq -r .test_suite_url) | |
| echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT" | |
| if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then | |
| echo "β Failed to upload test APK" | |
| echo "Response: $TEST_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "β Test APK uploaded: $TEST_URL" | |
| - 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 (same approach as Kotlin) | |
| TITLE="${{ env.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, | |
| \"instrumentationLogs\": 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 }}" | |
| if [ "$BUILD_ID" = "null" ] || [ -z "$BUILD_ID" ]; then | |
| echo "β No valid BUILD_ID available" | |
| exit 1 | |
| fi | |
| MAX_WAIT_TIME=1200 # 20 minutes | |
| CHECK_INTERVAL=30 # Check every 30 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/espresso/v2/builds/$BUILD_ID") | |
| STATUS=$(echo "$RESPONSE" | jq -r .status) | |
| if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then | |
| echo "β οΈ API error, retrying... (${ELAPSED}s elapsed)" | |
| sleep $CHECK_INTERVAL | |
| ELAPSED=$((ELAPSED + CHECK_INTERVAL)) | |
| continue | |
| fi | |
| echo "π Status: $STATUS (${ELAPSED}s elapsed)" | |
| # Check for completion | |
| if [[ "$STATUS" =~ ^(done|failed|error|passed|completed)$ ]]; then | |
| echo "β Build completed with status: $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 results:" | |
| echo "$FINAL_RESULT" | jq . | |
| # Validate and check results | |
| 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 "β Tests failed with status: $BUILD_STATUS" | |
| FAILED_DEVICES=$(echo "$FINAL_RESULT" | jq -r '.devices[] | select(.sessions[].status != "passed") | .device') | |
| if [ -n "$FAILED_DEVICES" ]; then | |
| echo "Failed on devices: $FAILED_DEVICES" | |
| fi | |
| exit 1 | |
| else | |
| echo "π All tests passed successfully!" | |
| fi | |
| else | |
| echo "β οΈ Could not parse final results" | |
| exit 1 | |
| fi | |
| - name: Upload test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: | | |
| android-java/app/build/outputs/apk/ | |
| android-java/app/build/reports/ | |
| - name: Comment PR with results | |
| if: github.event_name == 'pull_request' && always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const testDocId = 'Generated during BrowserStack testing'; | |
| const status = '${{ job.status }}'; | |
| const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; | |
| const body = `## π± BrowserStack Test Results (Android Java) | |
| **Status:** ${status === 'success' ? 'β Passed' : 'β Failed'} | |
| **Build:** [#${{ github.run_number }}](${runUrl}) | |
| **Test Document ID:** ${testDocId || 'Not generated'} | |
| ### Tested Devices: | |
| - Google Pixel 8 (Android 14) | |
| - Samsung Galaxy S23 (Android 13) | |
| - Google Pixel 6 (Android 12) | |
| - OnePlus 9 (Android 11) | |
| ### Test Verification: | |
| - β Lint check completed | |
| - β APK build successful | |
| - β Unit tests passed | |
| - β Test document seeded to Ditto Cloud | |
| - ${status === 'success' ? 'β ' : 'β'} Integration test verification on BrowserStack | |
| `; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); |