Skip to content

Fix BrowserStack Automate integration to follow official patterns #90

Fix BrowserStack Automate integration to follow official patterns

Fix BrowserStack Automate integration to follow official patterns #90

Workflow file for this run

#
# .github/workflows/android-java-ci.yml
# Workflow for building and testing android-java on BrowserStack physical devices
#
---
name: android-java-ci
on:
pull_request:
branches: [main]
paths:
- 'android-java/**'
- '.github/workflows/android-java-ci.yml'
push:
branches: [main]
paths:
- 'android-java/**'
- '.github/workflows/android-java-ci.yml'
workflow_dispatch: # Allow manual trigger
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-and-test:
name: Build and Test
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 30
outputs:
test_doc_title: ${{ steps.test_doc.outputs.test_doc_title }}
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: Generate test document title
id: test_doc
run: |
# Create a unique GitHub test document with inverted timestamp to appear at top
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 "test_doc_id=$DOC_ID" >> $GITHUB_OUTPUT
echo "test_doc_title=$DOC_TITLE" >> $GITHUB_OUTPUT
echo "📝 Generated test document (inverted timestamp for top position)"
echo "📝 ID: '${DOC_ID}'"
echo "📝 Title: '${DOC_TITLE}'"
echo "📝 Timestamp: ${TIMESTAMP} → Inverted: ${INVERTED_TIMESTAMP}"
- name: Build APKs
working-directory: android-java
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 }}
# Set DITTO_ENABLE_CLOUD_SYNC=true for integration tests
DITTO_ENABLE_CLOUD_SYNC: true
run: ./gradlew assembleDebug assembleDebugAndroidTest
- name: Run unit tests
working-directory: android-java
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 }}
DITTO_ENABLE_CLOUD_SYNC: false # Default to false for local testing
run: ./gradlew test
- 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
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports-${{ github.run_number }}
path: android-java/app/build/reports/
retention-days: 1
browserstack-test:
name: BrowserStack Device Testing
runs-on: ubuntu-latest
needs: build-and-test
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/

Check failure on line 164 in .github/workflows/android-java-ci.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/android-java-ci.yml

Invalid workflow file

You have an error in your yaml syntax on line 164
- name: Insert test document into Ditto Cloud
run: |
# Use the test document title from build-and-test job
TEST_DOC_TITLE="${{ needs.build-and-test.outputs.test_doc_title }}"
DOC_ID="ci_test_${GITHUB_RUN_ID}_${GITHUB_RUN_NUMBER}"
# Insert test document using Ditto API
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\": \"${TEST_DOC_TITLE}\",
\"done\": false,
\"deleted\": false
}
}
}" \
"https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then
echo "✓ Test document inserted successfully: ${DOC_ID}"
echo "✓ Seed title: ${TEST_DOC_TITLE}"
echo "GITHUB_TEST_DOC_ID=${TEST_DOC_TITLE}" >> $GITHUB_ENV
else
echo "❌ Failed to insert test document (HTTP ${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
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\": \"${{ env.GITHUB_TEST_DOC_ID }}\"
}
}")
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 (reduced from 30 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: Generate test report
if: always()
run: |
BUILD_ID="${{ steps.test.outputs.build_id }}"
# Create test report
echo "# BrowserStack 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
if echo "$RESULTS" | jq -e .devices > /dev/null 2>&1; then
echo "$RESULTS" | jq -r '.devices[] | "- \(.device): \(.status)"' >> test-report.md
else
echo "Unable to retrieve device results" >> test-report.md
fi
echo "" >> test-report.md
echo "## Sync Verification" >> 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: test-results
path: |
android-java/app/build/outputs/apk/
android-java/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 }}';
const testDocId = '${{ env.GITHUB_TEST_DOC_ID }}';
let body;
if (buildId === 'null' || buildId === '' || !buildId) {
body = `## 📱 BrowserStack Test Results (Android Java)
**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)
`;
} else {
const bsUrl = `https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`;
body = `## 📱 BrowserStack Test Results (Android Java)
**Status:** ${status === 'success' ? '✅ Passed' : '❌ Failed'}
**Build:** [#${{ github.run_number }}](${runUrl})
**BrowserStack:** [View detailed results](${bsUrl})
**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
});