Skip to content

ci(android-kotlin): add BrowserStack integration tests #59

ci(android-kotlin): add BrowserStack integration tests

ci(android-kotlin): add BrowserStack integration tests #59

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
});