Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9a16878
feat: add Android Kotlin CI pipeline with lint step
teodorciuraru Sep 3, 2025
cd1b64f
feat: add BrowserStack integration with Ditto Cloud sync testing
teodorciuraru Sep 3, 2025
aa2624f
fix: improve integration test error handling and activity lifecycle
teodorciuraru Sep 3, 2025
13f71c1
chore: remove local seeding functionality and simplify integration tests
teodorciuraru Sep 3, 2025
56a9c4e
fix: replace Compose integration tests with working ActivityScenario …
teodorciuraru Sep 3, 2025
79b0809
fix: configure BrowserStack to run only working SimpleIntegrationTest
teodorciuraru Sep 3, 2025
a4f6165
feat: make integration tests strict to prevent false positives
teodorciuraru Sep 3, 2025
ea8dc80
feat: adopt working Compose test pattern while keeping document seeding
teodorciuraru Sep 3, 2025
5d373e7
fix: correct BrowserStack API test filter format
teodorciuraru Sep 3, 2025
4b336f8
test: add intentional failure to verify BrowserStack can detect failures
teodorciuraru Sep 3, 2025
42aad37
fix: remove intentional failure test, return to production-ready state
teodorciuraru Sep 3, 2025
3cef968
refactor: prepare Android Kotlin BrowserStack CI for production
teodorciuraru Sep 3, 2025
eff3797
feat: consolidate Android Kotlin CI workflows to eliminate duplicate …
teodorciuraru Sep 3, 2025
b930005
perf: optimize BrowserStack test timing - exit immediately when docum…
teodorciuraru Sep 4, 2025
bf070be
feat: add 3-second visual pause after document detection for BrowserS…
teodorciuraru Sep 4, 2025
9614e3e
fix: ensure document sync test verifies actual app launch and UI context
teodorciuraru Sep 4, 2025
c5ce570
feat: improve document ordering and testing with title-based comparison
teodorciuraru Sep 4, 2025
970e7cb
refactor: streamline CI tests - minimal seeding, lightweight checks
teodorciuraru Sep 4, 2025
49e44f5
fix: enable BrowserStack tests on PRs and all pushes
teodorciuraru Sep 4, 2025
bc71926
feat: integrate working BrowserStack logic from PR #123 into consolid…
teodorciuraru Sep 4, 2025
3e6ef9b
fix: download APK artifacts to correct path structure for BrowserStac…
teodorciuraru Sep 4, 2025
1180bf8
fix: remove redundant final check after document detection
teodorciuraru Sep 4, 2025
ad2a7bc
feat: use inverted timestamp for CI test documents to always appear o…
teodorciuraru Sep 4, 2025
129cee4
refactor: remove extraneous reporting and seeding logic
teodorciuraru Sep 4, 2025
6f9ab84
refactor: replace custom integration tests with working TasksUITest f…
teodorciuraru Sep 4, 2025
85a52da
chore: remove local seeding scripts and testing documentation
teodorciuraru Sep 4, 2025
c8add20
feat: implement exact task title matching for BrowserStack tests
teodorciuraru Sep 4, 2025
b8e191f
feat: implement exact title matching following Swift workflow pattern
teodorciuraru Sep 4, 2025
8db363b
fix: ensure test fails when exact document title is not found
teodorciuraru Sep 4, 2025
361e6e3
test: verify local test failure mechanism works correctly
teodorciuraru Sep 4, 2025
0ceef67
fix: use exact text matching instead of substring matching
teodorciuraru Sep 4, 2025
3a034ad
debug: add better error handling for BrowserStack test issues
teodorciuraru Sep 4, 2025
03370a2
fix: use build-time configuration instead of runtime environment vari…
teodorciuraru Sep 4, 2025
fdcb614
fix: correct hasAnyChild() matcher usage in test debugging code
teodorciuraru Sep 4, 2025
925badb
feat: implement BrowserStack instrumentationOptions for parameter pas…
teodorciuraru Sep 4, 2025
f64a4e5
fix: use ascending-friendly document ID for proper list ordering
teodorciuraru Sep 4, 2025
91b644e
fix: use inverted timestamp format for document ID to match Swift wor…
teodorciuraru Sep 4, 2025
d734494
feat: add robust integration test and improve task ordering
teodorciuraru Sep 4, 2025
99c794f
refactor: clean up integration test for production
teodorciuraru Sep 4, 2025
f7b2d54
perf: optimize test timing based on PR review feedback
teodorciuraru Sep 4, 2025
afa8549
perf: eliminate all Thread.sleep calls from test
teodorciuraru Sep 4, 2025
18c8e8e
refactor: improve exception handling specificity
teodorciuraru Sep 8, 2025
1e74c44
refactor: remove hardcoded fallback and enforce proper configuration
teodorciuraru Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 351 additions & 0 deletions .github/workflows/android-kotlin-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
name: Android Kotlin CI

on:
push:
branches: [ main ]
paths:
- 'android-kotlin/**'
- '.github/workflows/android-kotlin-ci.yml'
pull_request:
branches: [ main ]
paths:
- 'android-kotlin/**'
- '.github/workflows/android-kotlin-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-kotlin/QuickStartTasks/.gradle
key: gradle-${{ runner.os }}-${{ hashFiles('android-kotlin/QuickStartTasks/gradle/wrapper/gradle-wrapper.properties', 'android-kotlin/QuickStartTasks/**/*.gradle*', 'android-kotlin/QuickStartTasks/gradle/libs.versions.toml') }}
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-kotlin/QuickStartTasks
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-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 }}
TEST_DOCUMENT_TITLE: ${{ steps.test_doc.outputs.test_doc_title }}
run: ./gradlew assembleDebug assembleDebugAndroidTest

- 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 APK artifacts
uses: actions/upload-artifact@v4
with:
name: android-apks-${{ github.run_number }}
path: |
android-kotlin/QuickStartTasks/app/build/outputs/apk/debug/app-debug.apk
android-kotlin/QuickStartTasks/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-kotlin/QuickStartTasks/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-kotlin/QuickStartTasks/app/build/outputs/apk/

- name: Insert test document into Ditto Cloud
run: |
# Use the same document title that was built into the APK
DOC_TITLE="${{ needs.build-and-test.outputs.test_doc_title }}"
DOC_ID="$DOC_TITLE"

echo "📝 Inserting test document that matches build-time configuration"
echo "📝 ID: '${DOC_ID}'"
echo "📝 Title: '${DOC_TITLE}'"

# Insert document using Ditto API v4 (same as Swift 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 }}"

# 1. Upload AUT (app-debug.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")
APP_URL=$(echo "$APP_UPLOAD_RESPONSE" | jq -r .app_url)
echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT"

# 2. Upload Espresso test-suite (app-debug-androidTest.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")
TEST_URL=$(echo "$TEST_UPLOAD_RESPONSE" | jq -r .test_suite_url)
echo "test_url=$TEST_URL" >> "$GITHUB_OUTPUT"

- 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 (correct approach for Android)
TITLE="${{ needs.build-and-test.outputs.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 Kotlin\",
\"buildName\": \"Build #${{ github.run_number }}\",
\"buildTag\": \"${{ github.ref_name }}\",
\"deviceLogs\": true,
\"video\": true,
\"networkLogs\": true,
\"autoGrantPermissions\": 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 }}"

# Validate BUILD_ID before proceeding
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)

# Check for API errors
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)"
echo "Full response: $BUILD_STATUS_RESPONSE"

# Check for completion states - BrowserStack uses different status values
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 .

# Check if we got valid results
if echo "$FINAL_RESULT" | jq -e .devices > /dev/null 2>&1; then
# Check if the overall build passed
BUILD_STATUS=$(echo "$FINAL_RESULT" | jq -r .status)
if [ "$BUILD_STATUS" != "passed" ]; then
echo "Build failed with status: $BUILD_STATUS"

# Check each device for failures
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
Loading
Loading