Skip to content

Bump Ditto SDK to 4.13.1 #252

Bump Ditto SDK to 4.13.1

Bump Ditto SDK to 4.13.1 #252

Workflow file for this run

name: Flutter CI
on:
pull_request:
paths:
- "flutter_app/**"
- ".github/workflows/flutter-ci.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file for linting
run: |
echo "DITTO_APP_ID=test" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=test" >> flutter_app/.env
echo "DITTO_AUTH_URL=test" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=test" >> flutter_app/.env
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Check formatting
working-directory: flutter_app
run: |
dart format --set-exit-if-changed .
if [ $? -ne 0 ]; then
echo "❌ Code formatting issues found. Run 'dart format .' to fix."
exit 1
fi
- name: Analyze code
working-directory: flutter_app
run: flutter analyze --no-fatal-infos
build-android:
name: Build Android
runs-on: ubuntu-latest
needs: [lint, unit-tests]
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
flutter_app/.dart_tool
flutter_app/build
key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }}
restore-keys: |
flutter-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Build Android APK (sanity check)
working-directory: flutter_app
run: flutter build apk --release
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: flutter-android-apk-${{ github.run_number }}
path: flutter_app/build/app/outputs/flutter-apk/app-release.apk
retention-days: 1
build-ios:
name: Build iOS
runs-on: macos-latest
needs: [lint, unit-tests]
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
flutter_app/.dart_tool
flutter_app/build
key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }}
restore-keys: |
flutter-${{ runner.os }}-
- name: Cache CocoaPods dependencies
uses: actions/cache@v4
with:
path: flutter_app/ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('flutter_app/ios/Podfile.lock') }}
restore-keys: |
pods-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Install CocoaPods
working-directory: flutter_app/ios
run: pod install || pod install --repo-update
- name: Build iOS (no signing)
working-directory: flutter_app
run: flutter build ios --release --no-codesign
- name: Create unsigned IPA
working-directory: flutter_app
run: |
mkdir -p build/ios/iphoneos/Payload
cp -r build/ios/iphoneos/Runner.app build/ios/iphoneos/Payload/
cd build/ios/iphoneos
zip -r Runner-unsigned.ipa Payload/
echo "✅ Created unsigned IPA"
- name: Upload iOS build artifact
uses: actions/upload-artifact@v4
with:
name: flutter-ios-ipa-${{ github.run_number }}
path: flutter_app/build/ios/iphoneos/Runner-unsigned.ipa
retention-days: 1
build-web:
name: Build Web
runs-on: ubuntu-latest
needs: [lint, unit-tests]
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
flutter_app/.dart_tool
flutter_app/build
key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }}
restore-keys: |
flutter-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Build Web
working-directory: flutter_app
run: flutter build web --release
- name: Upload Web build artifact
uses: actions/upload-artifact@v4
with:
name: flutter-web-${{ github.run_number }}
path: flutter_app/build/web/
retention-days: 1
build-macos:
name: Build macOS Desktop
runs-on: macos-latest
needs: [lint, unit-tests]
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: |
~/.pub-cache
flutter_app/.dart_tool
flutter_app/build
key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }}
restore-keys: |
flutter-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Build macOS Desktop
working-directory: flutter_app
run: flutter build macos --release
- name: Upload macOS build artifact
uses: actions/upload-artifact@v4
with:
name: flutter-macos-${{ github.run_number }}
path: flutter_app/build/macos/Build/Products/Release/
retention-days: 1
build-windows:
name: Build Windows Desktop
runs-on: windows-latest
needs: [lint, unit-tests]
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Flutter dependencies
uses: actions/cache@v4
with:
path: |
~\AppData\Local\Pub\Cache
flutter_app\.dart_tool
flutter_app\build
key: flutter-${{ runner.os }}-${{ hashFiles('flutter_app/pubspec.lock') }}
restore-keys: |
flutter-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Build Windows Desktop
working-directory: flutter_app
run: flutter build windows --release
- name: Upload Windows build artifact
uses: actions/upload-artifact@v4
with:
name: flutter-windows-${{ github.run_number }}
path: flutter_app/build/windows/x64/runner/Release/
retention-days: 1
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=test" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=test" >> flutter_app/.env
echo "DITTO_AUTH_URL=test" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=test" >> flutter_app/.env
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Run tests
working-directory: flutter_app
run: |
if [ -d "test" ]; then
flutter test --coverage
else
echo "⚠️ No tests found. Skipping test execution."
fi
- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: flutter-coverage-${{ github.run_number }}
path: flutter_app/coverage/
retention-days: 1
browserstack-android:
name: BrowserStack Android Testing
runs-on: ubuntu-latest
needs: [build-android]
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
timeout-minutes: 150
outputs:
build_id: ${{ steps.test.outputs.build_id }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
flutter_app/android/.gradle
key: gradle-${{ runner.os }}-${{ hashFiles('flutter_app/android/gradle/wrapper/gradle-wrapper.properties', 'flutter_app/android/build.gradle.kts', 'flutter_app/android/app/build.gradle.kts') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Get BrowserStack build info (Android)
id: build-info-android
uses: ./.github/actions/generate-browserstack-names
with:
platform-suffix: ' (Android)'
title-max-length: '90'
commit-max-length: '130'
- name: Seed, build, upload, and execute BrowserStack Android tests
id: test
uses: nick-fields/retry@v3
with:
max_attempts: 5
timeout_minutes: 20
retry_wait_seconds: 900
command: |
# Seed test task to Ditto Cloud
echo "📝 Seeding test task to Ditto Cloud..."
TIMESTAMP=$(date +%s)
INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP))
SEED_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\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\",
\"title\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\",
\"done\": false,
\"deleted\": false
}
}
}" \
"https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute")
HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1)
BODY=$(echo "$SEED_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then
TASK_TITLE="${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}"
echo "✅ Document inserted successfully with title: $TASK_TITLE"
else
echo "❌ Failed to insert document. HTTP status: $HTTP_CODE"
echo "Response: $BODY"
exit 1
fi
# Build Android debug APKs with seeded task title
cd flutter_app/android
ENCODED_TASK=$(echo -n "TASK_TO_FIND=$TASK_TITLE" | base64)
TARGET_PATH="$(pwd)/../integration_test/app_test.dart"
echo "📦 Building Android debug APKs with task title: $TASK_TITLE"
echo "📦 Encoded: $ENCODED_TASK"
echo "📦 Target: $TARGET_PATH"
chmod +x gradlew
./gradlew assembleDebug assembleDebugAndroidTest \
-Ptarget="$TARGET_PATH" \
-Pdart-defines="SU5URUdSQVRJT05fVEVTVF9NT0RFPXRydWU=,$ENCODED_TASK"
cd ../..
# Upload app APK to BrowserStack
echo "📤 Uploading app APK to BrowserStack..."
APP_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
-X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/app" \
-F "file=@flutter_app/build/app/outputs/apk/debug/app-debug.apk")
echo "App upload response: $APP_RESPONSE"
APP_URL=$(echo "$APP_RESPONSE" | yq eval -p=json .app_url)
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 suite APK to BrowserStack
echo "📤 Uploading test suite APK to BrowserStack..."
TEST_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
-X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/test-suite" \
-F "file=@flutter_app/build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk")
echo "Test suite upload response: $TEST_RESPONSE"
TEST_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_suite_url)
if [ "$TEST_URL" = "null" ] || [ -z "$TEST_URL" ]; then
echo "❌ Failed to upload test suite APK"
echo "Response: $TEST_RESPONSE"
exit 1
fi
echo "✅ Test suite APK uploaded: $TEST_URL"
# Execute tests on BrowserStack
echo "🚀 Executing tests on BrowserStack..."
# Load devices from centralized config
DEVICES=$(yq eval -o=json -I=0 '.flutter.android.devices' .github/browserstack-devices.yml)
# Create test execution request
BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
-X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/android/build" \
-H "Content-Type: application/json" \
-d "{
\"app\": \"$APP_URL\",
\"testSuite\": \"$TEST_URL\",
\"devices\": $DEVICES,
\"project\": \"${{ steps.build-info-android.outputs.project-name }}\",
\"buildName\": \"${{ steps.build-info-android.outputs.build-name }}\",
\"buildTag\": \"${{ github.head_ref || github.ref_name }}\",
\"deviceLogs\": true,
\"video\": true,
\"networkLogs\": true,
\"autoGrantPermissions\": true
}")
echo "BrowserStack API Response:"
echo "$BUILD_RESPONSE"
BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .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"
# Wait for tests to complete
MAX_WAIT_TIME=1080 # 18 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/flutter-integration-tests/v2/android/builds/$BUILD_ID")
STATUS=$(echo "$RESPONSE" | yq eval .status)
if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then
echo "⚠️ API error, retrying... (${ELAPSED}s elapsed)"
sleep $CHECK_INTERVAL
ELAPSED=$((ELAPSED + CHECK_INTERVAL))
continue
fi
# Check device statuses - if all devices are done, we're done
DEVICE_STATUSES=$(echo "$RESPONSE" | yq eval '.devices[]?.sessions[]?.status' 2>/dev/null)
if [ -n "$DEVICE_STATUSES" ]; then
ALL_DONE=true
while IFS= read -r dev_status; do
if [[ ! "$dev_status" =~ ^(passed|failed|error|done)$ ]]; then
ALL_DONE=false
break
fi
done <<< "$DEVICE_STATUSES"
if [ "$ALL_DONE" = true ]; then
echo "✅ All devices completed (${ELAPSED}s elapsed)"
break
fi
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/flutter-integration-tests/v2/android/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" | yq eval .status)
if [ "$BUILD_STATUS" != "passed" ]; then
echo "❌ Tests failed with status: $BUILD_STATUS"
FAILED_DEVICES=$(echo "$FINAL_RESULT" | yq eval '.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!"
echo "Dashboard URL: https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID"
fi
else
echo "⚠️ Could not parse final results"
exit 1
fi
- name: Add to GitHub Actions Summary
if: always()
run: |
BUILD_ID="${{ steps.test.outputs.build_id }}"
echo "### 🤖 BrowserStack Android Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ job.status }}" = "success" ]; then
echo "✅ **Status:** Passed" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Status:** Failed" >> $GITHUB_STEP_SUMMARY
fi
if [ -n "$BUILD_ID" ]; then
echo "- **Build ID:** $BUILD_ID" >> $GITHUB_STEP_SUMMARY
echo "- **Dashboard:** [View Results](https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID/)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
browserstack-ios:
name: BrowserStack iOS Testing
runs-on: macos-latest
needs: [build-ios]
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.x"
channel: "stable"
- name: Create .env file
run: |
echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > flutter_app/.env
echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> flutter_app/.env
echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> flutter_app/.env
echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> flutter_app/.env
- name: Get dependencies
working-directory: flutter_app
run: flutter pub get
- name: Install CocoaPods
working-directory: flutter_app/ios
run: pod install || pod install --repo-update
- name: Download iOS IPA
uses: actions/download-artifact@v4
with:
name: flutter-ios-ipa-${{ github.run_number }}
path: flutter_app/build/ios/ipa/
- name: Get BrowserStack build info (iOS)
id: build-info-ios
uses: ./.github/actions/generate-browserstack-names
with:
platform-suffix: ' (iOS)'
title-max-length: '90'
commit-max-length: '130'
- name: Seed, build, upload, and execute BrowserStack iOS tests
id: test
uses: nick-fields/retry@v3
with:
max_attempts: 5
timeout_minutes: 20
retry_wait_seconds: 900
command: |
# Seed test task to Ditto Cloud
echo "📝 Seeding test task to Ditto Cloud..."
TIMESTAMP=$(date +%s)
INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP))
SEED_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\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\",
\"title\": \"${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}\",
\"done\": false,
\"deleted\": false
}
}
}" \
"https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute")
HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1)
BODY=$(echo "$SEED_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then
TASK_TITLE="${INVERTED_TIMESTAMP}_flutter_ci_test_${{ github.run_id }}_${{ github.run_number }}"
echo "✅ Document inserted successfully with title: $TASK_TITLE"
else
echo "❌ Failed to insert document. HTTP status: $HTTP_CODE"
echo "Response: $BODY"
exit 1
fi
# Build iOS integration test package with seeded task title
cd flutter_app
echo "🧪 Building iOS integration test package with task title: $TASK_TITLE"
flutter build ios integration_test/app_test.dart --release --no-codesign \
--dart-define=INTEGRATION_TEST_MODE=true \
--dart-define="TASK_TO_FIND=$TASK_TITLE"
# Create iOS test package
cd ..
output="../build/ios_integration"
product="build/ios_integration/Build/Products"
pushd flutter_app/ios
xcodebuild -workspace Runner.xcworkspace \
-scheme Runner \
-config Flutter/Release.xcconfig \
-derivedDataPath $output \
-sdk iphoneos \
build-for-testing \
CODE_SIGNING_ALLOWED=NO
popd
pushd flutter_app/$product
XCTESTRUN_FILE=$(find . -name "*.xctestrun" -type f | head -1)
if [ -z "$XCTESTRUN_FILE" ]; then
echo "❌ No .xctestrun file found"
exit 1
fi
echo "📦 Found xctestrun file: $XCTESTRUN_FILE"
zip -r "ios_test_package.zip" "Release-iphoneos" "$XCTESTRUN_FILE"
popd
# Upload iOS test package to BrowserStack
echo "📤 Uploading iOS test package to BrowserStack..."
TEST_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
-X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/test-package" \
-F "file=@flutter_app/build/ios_integration/Build/Products/ios_test_package.zip")
echo "Upload response: $TEST_RESPONSE"
TEST_PACKAGE_URL=$(echo "$TEST_RESPONSE" | yq eval -p=json .test_package_url)
if [ "$TEST_PACKAGE_URL" = "null" ] || [ -z "$TEST_PACKAGE_URL" ]; then
echo "❌ Failed to upload iOS test package"
echo "Response: $TEST_RESPONSE"
exit 1
fi
echo "✅ iOS test package uploaded: $TEST_PACKAGE_URL"
# Execute tests on BrowserStack
echo "🚀 Executing tests on BrowserStack..."
# Create test execution request
# NOTE: Flutter testing framework requires iOS 15+ due to _backtrace_async symbol
# See: https://developer.apple.com/documentation/os/backtrace_async
# Load devices from centralized config
DEVICES=$(yq eval -o=json -I=0 '.flutter.ios.devices' .github/browserstack-devices.yml)
BUILD_RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
-X POST "https://api-cloud.browserstack.com/app-automate/flutter-integration-tests/v2/ios/build" \
-H "Content-Type: application/json" \
-d "{
\"testPackage\": \"$TEST_PACKAGE_URL\",
\"devices\": $DEVICES,
\"project\": \"${{ steps.build-info-ios.outputs.project-name }}\",
\"buildName\": \"${{ steps.build-info-ios.outputs.build-name }}\",
\"buildTag\": \"${{ github.head_ref || github.ref_name }}\",
\"deviceLogs\": true,
\"networkLogs\": true,
\"autoGrantPermissions\": true
}")
echo "BrowserStack API Response:"
echo "$BUILD_RESPONSE"
BUILD_ID=$(echo "$BUILD_RESPONSE" | yq eval .build_id)
echo "build_id=$BUILD_ID" >> "$GITHUB_OUTPUT"
# 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 started with ID: $BUILD_ID"
# Wait for tests to complete
MAX_WAIT_TIME=1080 # 18 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/flutter-integration-tests/v2/ios/builds/$BUILD_ID")
STATUS=$(echo "$RESPONSE" | yq eval .status)
if [ "$STATUS" = "null" ] || [ -z "$STATUS" ]; then
echo "⚠️ API error, retrying... (${ELAPSED}s elapsed)"
sleep $CHECK_INTERVAL
ELAPSED=$((ELAPSED + CHECK_INTERVAL))
continue
fi
echo "📊 Build Status: $STATUS (${ELAPSED}s elapsed)"
# Check device statuses - if all devices are done, we're done
DEVICE_STATUSES=$(echo "$RESPONSE" | yq eval '.devices[]?.sessions[]?.status' 2>/dev/null)
if [ -n "$DEVICE_STATUSES" ]; then
ALL_DONE=true
while IFS= read -r dev_status; do
if [[ ! "$dev_status" =~ ^(passed|failed|error|done)$ ]]; then
ALL_DONE=false
break
fi
done <<< "$DEVICE_STATUSES"
if [ "$ALL_DONE" = true ]; then
echo "✅ All devices completed (${ELAPSED}s elapsed)"
break
fi
fi
# Check for build-level completion as fallback
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/flutter-integration-tests/v2/ios/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" | yq eval .status)
if [ "$BUILD_STATUS" != "passed" ]; then
echo "❌ Tests failed with status: $BUILD_STATUS"
FAILED_DEVICES=$(echo "$FINAL_RESULT" | yq eval '.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!"
echo "Dashboard URL: https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID"
fi
else
echo "⚠️ Could not parse final results"
exit 1
fi
- name: Add to GitHub Actions Summary
if: always()
run: |
BUILD_ID="${{ steps.test.outputs.build_id }}"
echo "### 🍎 BrowserStack iOS Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ job.status }}" = "success" ]; then
echo "✅ **Status:** Passed" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Status:** Failed" >> $GITHUB_STEP_SUMMARY
fi
if [ -n "$BUILD_ID" ]; then
echo "- **Build ID:** $BUILD_ID" >> $GITHUB_STEP_SUMMARY
echo "- **Dashboard:** [View Results](https://app-automate.browserstack.com/dashboard/v2/builds/$BUILD_ID/)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
summary:
name: Test Summary
runs-on: ubuntu-latest
needs:
[
lint,
unit-tests,
build-android,
build-ios,
build-web,
build-macos,
build-windows,
browserstack-android,
browserstack-ios,
]
if: always()
steps:
- name: Create Overall Summary
run: |
echo "## 📊 BrowserStack Test Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check overall status
if [ "${{ needs.browserstack-android.result }}" = "success" ] && [ "${{ needs.browserstack-ios.result }}" = "success" ]; then
echo "🎉 **Overall Status:** All tests passed!" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.browserstack-android.result }}" = "skipped" ] || [ "${{ needs.browserstack-ios.result }}" = "skipped" ]; then
echo "⚠️ **Overall Status:** Some tests were skipped" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Overall Status:** Some tests failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Results:" >> $GITHUB_STEP_SUMMARY
echo "- **Android:** ${{ needs.browserstack-android.result }}" >> $GITHUB_STEP_SUMMARY
echo "- **iOS:** ${{ needs.browserstack-ios.result }}" >> $GITHUB_STEP_SUMMARY