From f9896f660c87926752937b4b4c0944a1b4c4f89b Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 30 Apr 2026 13:13:52 +0100 Subject: [PATCH 01/45] introduce initial maestro flow --- maestro/e2e-tests.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 maestro/e2e-tests.yml diff --git a/maestro/e2e-tests.yml b/maestro/e2e-tests.yml new file mode 100644 index 0000000..313b527 --- /dev/null +++ b/maestro/e2e-tests.yml @@ -0,0 +1,9 @@ +appId: com.comapeo.core.testing +--- +- launchApp: + clearState: true +- tapOn: "Run tests" +- extendedWaitUntil: + visible: + id: "all-tests-passed" + timeout: 10000 From 4fa45288592c64e3a0199afced3a65b5c323c721 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 30 Apr 2026 14:25:00 +0100 Subject: [PATCH 02/45] record test --- .gitignore | 3 +++ maestro/e2e-tests.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index cdfe0ae..e3687e8 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ build/ # eslint .eslintcache + +# Maestro artifacts +maestro/recordings/ diff --git a/maestro/e2e-tests.yml b/maestro/e2e-tests.yml index 313b527..7ba9af5 100644 --- a/maestro/e2e-tests.yml +++ b/maestro/e2e-tests.yml @@ -2,8 +2,11 @@ appId: com.comapeo.core.testing --- - launchApp: clearState: true +- startRecording: + path: "maestro/recordings/e2e-tests" - tapOn: "Run tests" - extendedWaitUntil: visible: id: "all-tests-passed" timeout: 10000 +- stopRecording From b4ea31bc570e2d79cfeaa48575e4b9262f7d5773 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Mon, 4 May 2026 12:47:00 +0100 Subject: [PATCH 03/45] update app id used in test --- maestro/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestro/e2e-tests.yml b/maestro/e2e-tests.yml index 7ba9af5..3621198 100644 --- a/maestro/e2e-tests.yml +++ b/maestro/e2e-tests.yml @@ -1,4 +1,4 @@ -appId: com.comapeo.core.testing +appId: com.comapeo.core.e2e --- - launchApp: clearState: true From b4ddadc45b91b729f5a147f15a4a945ff609e6fc Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Mon, 4 May 2026 15:50:14 +0100 Subject: [PATCH 04/45] rename maestro flow --- maestro/{e2e-tests.yml => e2e.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename maestro/{e2e-tests.yml => e2e.yml} (83%) diff --git a/maestro/e2e-tests.yml b/maestro/e2e.yml similarity index 83% rename from maestro/e2e-tests.yml rename to maestro/e2e.yml index 3621198..47c27f8 100644 --- a/maestro/e2e-tests.yml +++ b/maestro/e2e.yml @@ -3,7 +3,7 @@ appId: com.comapeo.core.e2e - launchApp: clearState: true - startRecording: - path: "maestro/recordings/e2e-tests" + path: "maestro/recordings/e2e" - tapOn: "Run tests" - extendedWaitUntil: visible: From 9ca36bed1ec62c67b7e572687dd70e4a5449a4e5 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 13:23:28 +0100 Subject: [PATCH 05/45] wip workflow still need to wait and poll test run statuses --- .github/workflows/e2e-tests.yml | 289 ++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 .github/workflows/e2e-tests.yml diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..2751c99 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,289 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODEJS_MOBILE_VERSION: v18.20.4 + +jobs: + build-android: + if: ${{ github.event.pull_request.draft == false }} + name: Build (Android) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + outputs: + app_url: ${{ steps.upload.outputs.app_url }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v6 + with: + node-version-file: package.json + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Cache nodejs-mobile binaries + id: cache-libnode + uses: actions/cache@v4 + with: + path: android/libnode + # hashFiles(...) folds the BASE_URL (defined inside the + # download script) into the cache key, so any swap of the + # source repo automatically invalidates stale caches. + key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-android-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} + + - name: Download nodejs-mobile binaries + if: steps.cache-libnode.outputs.cache-hit != 'true' + run: ./scripts/download-nodejs-mobile.sh + + - name: Install npm dependencies + run: | + npm install --ignore-scripts + npx patch-package + + - name: Expo prebuild (generates apps/e2e/android) + working-directory: apps/e2e + run: npx expo prebuild --platform android --no-install + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Set up E2E app + working-directory: apps/e2e + run: | + npm install --ignore-scripts + npx patch-package + npx expo prebuild --platform android --no-install + + - name: Build APK + working-directory: apps/e2e/android + run: | + ./gradlew assembleRelease --no-daemon + + - name: Upload APK + id: upload + env: + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + run: | + APK_PATH="$(pwd)/apps/e2e/android/app/build/outputs/apk/release/app-release.apk" + + APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$APK_PATH" | jq -r '.app_url') + + echo "app_url=$APP_URL" >> $GITHUB_OUTPUT + + build-ios: + if: ${{ github.event.pull_request.draft == false }} + name: Upload test suite + runs-on: macos-15 + timeout-minutes: 15 + permissions: + contents: read + outputs: + app_url: ${{ steps.upload.outputs.app_url }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer + + - uses: actions/setup-node@v6 + with: + node-version-file: package.json + + - name: Cache nodejs-mobile binaries + id: cache-libnode + uses: actions/cache@v4 + with: + path: android/libnode + # hashFiles(...) folds the BASE_URL (defined inside the + # download script) into the cache key, so any swap of the + # source repo automatically invalidates stale caches. + key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-android-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} + + - name: Download nodejs-mobile binaries + if: steps.cache-libnode.outputs.cache-hit != 'true' + run: ./scripts/download-nodejs-mobile.sh + + - name: Build backend bundle + run: npm run backend:build + + - name: Expo prebuild + working-directory: apps/e2e + run: npx expo prebuild --platform ios + + - name: Build .ipa + working-directory: apps/e2e/ios + run: | + xcodebuild archive \ + -workspace corereactnativee2e.xcworkspace \ + -scheme corereactnativee2e \ + -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -archivePath ./build/corereactnativee2e.xcarchive \ + CODE_SIGNING_ALLOWED='NO' + + mkdir -p Payload + cp -r ./build/corereactnativee2e.xcarchive/Products/Applications/corereactnativee2e.app Payload/ + zip -r corereactnativee2e.ipa Payload/ + + - name: Upload + id: upload + run: | + IPA_PATH="$(pwd)/apps/e2e/ios/corereactnativee2e.ipa" + + APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$IPA_PATH" | jq -r '.app_url') + + echo "app_url=$APP_URL" >> $GITHUB_OUTPUT + + upload-test-suite: + name: Upload test suite + needs: [build-android, build-ios] + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + test_suite_url: ${{ steps.upload.outputs.test_suite_url }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + sparse-checkout: | + maestro + + - name: Upload test suite to Browserstack + id: upload + env: + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + run: | + ZIP_NAME="maestro_tests.zip" + + zip -r "$ZIP_NAME" maestro/e2e.yml + + ZIP_PATH="$(pwd)/$ZIP_NAME" + + TEST_SUITE_URL=$(curl -u "BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ + -F "file=@$ZIP_PATH" | jq -r '.test_suite_url') + + echo "test_suite_url=$TEST_SUITE_URL" >> $GITHUB_ENV + + test-android: + name: Run test suite on Android + needs: [build-android, upload-test-suite] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Run tests + id: run + env: + APP_URL: ${{ needs.android-build.outputs.app_url }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_PROJECT_NAME: ${{ vars.BROWSERSTACK_PROJECT_NAME }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + TEST_SUITE_URL: ${{ needs.upload-test-suite.outputs.test_suite_url }} + run: | + DEVICES='[ + "Google Pixel 5-11.0", + "Google Pixel 9-16.0", + "Huawei P30-9.0", + "Huawei Nova 11 SE-12.0", + "Motorola Moto G9 Play-10.0", + "Motorola Moto G71 5G-11.0", + "OnePlus 9-11.0", + "OnePlus 11R-13.0", + "OnePlus 12R-14.0", + "OnePlus 13R-15.0", + "Oppo Reno 3 Pro-10.0", + "Oppo Reno 6-11.0", + "Samsung Galaxy Note 9-8.1" + "Samsung Galaxy S10-9.0", + "Samsung Galaxy A51-10.0", + "Samsung Galaxy S23-13.0", + "Vivo V21-11.0", + "Vivo Y21-11.0", + "Xiaomi Redmi Note 9-10.0" + "Xiaomi Redmi Note 11-11.0", + ]' + + BUILD_ID=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ + -d '{ + "app": "$APP_URL", + "testSuite": "$TEST_SUITE_URL", + "devices": $DEVICES, + "project": "$BROWSERSTACK_PROJECT_NAME", + "deviceLogs": true }' \ + -H "Content-Type: application/json" | jq -r '.build_id') + + echo "build_id=$BUILD_ID" >> $GITHUB_ENV + + test-ios: + name: Run test suite on iOS + needs: [build-ios, upload-test-suite] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Run tests + id: run + env: + APP_URL: ${{ needs.build-ios.outputs.app_url }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_PROJECT_NAME: ${{ vars.BROWSERSTACK_PROJECT_NAME }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + TEST_SUITE_URL: ${{ needs.upload-test-suite.outputs.test_suite_url }} + run: | + DEVICES='[ + "iPhone 13-15", + "iPhone 15-17", + "iPhone 17-26" + ]' + BUILD_ID=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ + -d '{ + "app": "$APP_URL", + "testSuite": "$TEST_SUITE_URL", + "devices": $DEVICES, + "project": "$BROWSERSTACK_PROJECT_NAME", + "deviceLogs": true }' \ + -H "Content-Type: application/json" | jq -r '.build_id') + + echo "build_id=$BUILD_ID" >> $GITHUB_ENV From bafab2b71149f211971682a42cc2f55d3c21ae9e Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 13:37:34 +0100 Subject: [PATCH 06/45] specify maestro version to use --- .github/workflows/e2e-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 2751c99..161cbcb 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -245,7 +245,8 @@ jobs: "testSuite": "$TEST_SUITE_URL", "devices": $DEVICES, "project": "$BROWSERSTACK_PROJECT_NAME", - "deviceLogs": true }' \ + "deviceLogs": true, + "maestroVersion": "latest" }' \ -H "Content-Type: application/json" | jq -r '.build_id') echo "build_id=$BUILD_ID" >> $GITHUB_ENV @@ -283,7 +284,8 @@ jobs: "testSuite": "$TEST_SUITE_URL", "devices": $DEVICES, "project": "$BROWSERSTACK_PROJECT_NAME", - "deviceLogs": true }' \ + "deviceLogs": true, + "maestroVersion": "latest" }' \ -H "Content-Type: application/json" | jq -r '.build_id') echo "build_id=$BUILD_ID" >> $GITHUB_ENV From d38fb7a43b479c7a36b2cefb2cecba5e8fcd96ca Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 13:46:32 +0100 Subject: [PATCH 07/45] remove stopRecording command Browserstack's Maestro support does not seem to support this command (https://www.browserstack.com/docs/app-automate/maestro/references/supported-commands) --- maestro/e2e.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/maestro/e2e.yml b/maestro/e2e.yml index 47c27f8..e864a84 100644 --- a/maestro/e2e.yml +++ b/maestro/e2e.yml @@ -9,4 +9,3 @@ appId: com.comapeo.core.e2e visible: id: "all-tests-passed" timeout: 10000 -- stopRecording From ab12dde708c87f29c041adaad9a4208fc156eeea Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 13:51:40 +0100 Subject: [PATCH 08/45] simplify test jobs --- .github/workflows/e2e-tests.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 161cbcb..18f5fe5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -198,14 +198,10 @@ jobs: name: Run test suite on Android needs: [build-android, upload-test-suite] runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 5 permissions: contents: read steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run tests id: run env: @@ -255,14 +251,10 @@ jobs: name: Run test suite on iOS needs: [build-ios, upload-test-suite] runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 5 permissions: contents: read steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run tests id: run env: From 2571dfaa4330806e44c9bb4e6da208864c8b8462 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 13:53:32 +0100 Subject: [PATCH 09/45] allow test suite upload to happen immediately --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 18f5fe5..a135b1b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -162,8 +162,8 @@ jobs: echo "app_url=$APP_URL" >> $GITHUB_OUTPUT upload-test-suite: + if: ${{ github.event.pull_request.draft == false }} name: Upload test suite - needs: [build-android, build-ios] runs-on: ubuntu-latest permissions: contents: read From 1135e42cab964b605715d3e195c383a5778c6ad1 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Tue, 5 May 2026 14:08:10 +0100 Subject: [PATCH 10/45] minor cleanup --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a135b1b..95e7be5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -239,7 +239,7 @@ jobs: -d '{ "app": "$APP_URL", "testSuite": "$TEST_SUITE_URL", - "devices": $DEVICES, + "devices": "$DEVICES", "project": "$BROWSERSTACK_PROJECT_NAME", "deviceLogs": true, "maestroVersion": "latest" }' \ @@ -274,7 +274,7 @@ jobs: -d '{ "app": "$APP_URL", "testSuite": "$TEST_SUITE_URL", - "devices": $DEVICES, + "devices": "$DEVICES", "project": "$BROWSERSTACK_PROJECT_NAME", "deviceLogs": true, "maestroVersion": "latest" }' \ From a8ffdcd648da7a905a2e08f1bffb10813286b721 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 10:07:51 +0100 Subject: [PATCH 11/45] fix test-android env --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 95e7be5..5c01996 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -205,7 +205,7 @@ jobs: - name: Run tests id: run env: - APP_URL: ${{ needs.android-build.outputs.app_url }} + APP_URL: ${{ needs.build-android.outputs.app_url }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} BROWSERSTACK_PROJECT_NAME: ${{ vars.BROWSERSTACK_PROJECT_NAME }} BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} From afadca4cfbd45ac8a8d55afd229b7e715c05f46f Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 11:57:25 +0100 Subject: [PATCH 12/45] add polling for result step --- .../run-browserstack-maestro/action.yml | 95 ++++++++++++ .github/workflows/e2e-tests.yml | 146 ++++++++---------- 2 files changed, 162 insertions(+), 79 deletions(-) create mode 100644 .github/actions/run-browserstack-maestro/action.yml diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml new file mode 100644 index 0000000..a39db49 --- /dev/null +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -0,0 +1,95 @@ +name: Run BrowserStack Maestro Tests + +description: Triggers a BrowserStack Maestro build and polls until completion + +inputs: + app_url: + description: BrowserStack app URL (bs://...) + required: true + test_suite_url: + description: BrowserStack test suite URL (bs://...) + required: true + devices: + description: JSON array of device strings + required: true + project_name: + description: BrowserStack project name + required: true + browserstack_username: + description: BrowserStack username + required: true + browserstack_access_key: + description: BrowserStack access key + required: true + timeout: + description: Test run timeout in seconds + required: false + default: "600" + +runs: + using: composite + steps: + - name: Validate inputs + if: ${{ fromJson(inputs.timeout) < 30 }} + shell: bash + run: | + echo "timeout must be at least 30 seconds" + exit 1 + + - name: Trigger build + id: trigger + env: + APP_URL: ${{ inputs.app_url }} + TEST_SUITE_URL: ${{ inputs.test_suite_url }} + DEVICES: ${{ inputs.devices }} + PROJECT_NAME: ${{ inputs.project_name }} + BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} + BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} + shell: bash + run: | + PAYLOAD=$(jq -n \ + --arg app "$APP_URL" \ + --arg suite "$TEST_SUITE_URL" \ + --argjson devices "$DEVICES" \ + --arg project "$PROJECT_NAME" \ + '{app: $app, testSuite: $suite, devices: $devices, project: $project, deviceLogs: true, maestroVersion: "latest"}') + + BUILD_ID=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + | jq -r '.build_id') + + echo "Triggered build: $BUILD_ID" + echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT + + - name: Poll for result + env: + BUILD_ID: ${{ steps.trigger.outputs.build_id }} + BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} + BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} + TIMEOUT: ${{ fromJson(inputs.timeout) }} + shell: bash + run: | + MAX_WAIT=$TIMEOUT + POLL_INTERVAL=30 + ELAPSED=0 + + while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do + RESPONSE=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$BUILD_ID") + + STATUS=$(echo "$RESPONSE" | jq -r '.status') + echo "[${ELAPSED}s] Build $BUILD_ID status: $STATUS" + + case "$STATUS" in + passed|completed) echo "Tests passed"; exit 0 ;; + failed) echo "Tests failed"; exit 1 ;; + esac + + sleep "$POLL_INTERVAL" + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + echo "Timed out after ${MAX_WAIT}s waiting for build $BUILD_ID" + exit 1 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5c01996..3368571 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -95,7 +95,7 @@ jobs: build-ios: if: ${{ github.event.pull_request.draft == false }} - name: Upload test suite + name: Build (iOS) runs-on: macos-15 timeout-minutes: 15 permissions: @@ -173,8 +173,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - sparse-checkout: | - maestro + sparse-checkout: maestro - name: Upload test suite to Browserstack id: upload @@ -188,96 +187,85 @@ jobs: ZIP_PATH="$(pwd)/$ZIP_NAME" - TEST_SUITE_URL=$(curl -u "BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + TEST_SUITE_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ -F "file=@$ZIP_PATH" | jq -r '.test_suite_url') - echo "test_suite_url=$TEST_SUITE_URL" >> $GITHUB_ENV + echo "test_suite_url=$TEST_SUITE_URL" >> $GITHUB_OUTPUT test-android: - name: Run test suite on Android + name: Run tests (Android) needs: [build-android, upload-test-suite] runs-on: ubuntu-latest - timeout-minutes: 5 + # TODO: Adjust based on actual timing + timeout-minutes: 60 permissions: contents: read steps: - - name: Run tests - id: run - env: - APP_URL: ${{ needs.build-android.outputs.app_url }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_PROJECT_NAME: ${{ vars.BROWSERSTACK_PROJECT_NAME }} - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - TEST_SUITE_URL: ${{ needs.upload-test-suite.outputs.test_suite_url }} - run: | - DEVICES='[ - "Google Pixel 5-11.0", - "Google Pixel 9-16.0", - "Huawei P30-9.0", - "Huawei Nova 11 SE-12.0", - "Motorola Moto G9 Play-10.0", - "Motorola Moto G71 5G-11.0", - "OnePlus 9-11.0", - "OnePlus 11R-13.0", - "OnePlus 12R-14.0", - "OnePlus 13R-15.0", - "Oppo Reno 3 Pro-10.0", - "Oppo Reno 6-11.0", - "Samsung Galaxy Note 9-8.1" - "Samsung Galaxy S10-9.0", - "Samsung Galaxy A51-10.0", - "Samsung Galaxy S23-13.0", - "Vivo V21-11.0", - "Vivo Y21-11.0", - "Xiaomi Redmi Note 9-10.0" - "Xiaomi Redmi Note 11-11.0", - ]' - - BUILD_ID=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ - -d '{ - "app": "$APP_URL", - "testSuite": "$TEST_SUITE_URL", - "devices": "$DEVICES", - "project": "$BROWSERSTACK_PROJECT_NAME", - "deviceLogs": true, - "maestroVersion": "latest" }' \ - -H "Content-Type: application/json" | jq -r '.build_id') - - echo "build_id=$BUILD_ID" >> $GITHUB_ENV + - uses: actions/checkout@v4 + with: + persist-credentials: false + sparse-checkout: .github/actions + + - uses: ./.github/actions/run-browserstack-maestro + with: + app_url: ${{ needs.build-android.outputs.app_url }} + test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} + project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME }} + browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + # TODO: Adjust based on actual timing + timeout: 1800 + devices: | + [ + "Google Pixel 5-11.0", + "Google Pixel 9-16.0", + "Huawei P30-9.0", + "Huawei Nova 11 SE-12.0", + "Motorola Moto G9 Play-10.0", + "Motorola Moto G71 5G-11.0", + "OnePlus 9-11.0", + "OnePlus 11R-13.0", + "OnePlus 12R-14.0", + "OnePlus 13R-15.0", + "Oppo Reno 3 Pro-10.0", + "Oppo Reno 6-11.0", + "Samsung Galaxy Note 9-8.1", + "Samsung Galaxy S10-9.0", + "Samsung Galaxy A51-10.0", + "Samsung Galaxy S23-13.0", + "Vivo V21-11.0", + "Vivo Y21-11.0", + "Xiaomi Redmi Note 9-10.0", + "Xiaomi Redmi Note 11-11.0" + ] test-ios: - name: Run test suite on iOS + name: Run tests (iOS) needs: [build-ios, upload-test-suite] runs-on: ubuntu-latest - timeout-minutes: 5 + # TODO: Adjust based on actual timing + timeout-minutes: 60 permissions: contents: read steps: - - name: Run tests - id: run - env: - APP_URL: ${{ needs.build-ios.outputs.app_url }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_PROJECT_NAME: ${{ vars.BROWSERSTACK_PROJECT_NAME }} - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - TEST_SUITE_URL: ${{ needs.upload-test-suite.outputs.test_suite_url }} - run: | - DEVICES='[ - "iPhone 13-15", - "iPhone 15-17", - "iPhone 17-26" - ]' - BUILD_ID=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ - -d '{ - "app": "$APP_URL", - "testSuite": "$TEST_SUITE_URL", - "devices": "$DEVICES", - "project": "$BROWSERSTACK_PROJECT_NAME", - "deviceLogs": true, - "maestroVersion": "latest" }' \ - -H "Content-Type: application/json" | jq -r '.build_id') - - echo "build_id=$BUILD_ID" >> $GITHUB_ENV + - uses: actions/checkout@v4 + with: + persist-credentials: false + sparse-checkout: .github/actions + + - uses: ./.github/actions/run-browserstack-maestro + with: + app_url: ${{ needs.build-ios.outputs.app_url }} + test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} + project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME }} + browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + # TODO: Adjust based on actual timing + timeout: 1800 + devices: | + [ + "iPhone 13-15", + "iPhone 15-17", + "iPhone 17-26" + ] From eb5cf3c3407768b17121bd8e08bb0242929c60b1 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:10:10 +0100 Subject: [PATCH 13/45] update secrets and vars names --- .github/workflows/e2e-tests.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3368571..67549db 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -82,8 +82,8 @@ jobs: - name: Upload APK id: upload env: - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | APK_PATH="$(pwd)/apps/e2e/android/app/build/outputs/apk/release/app-release.apk" @@ -178,8 +178,8 @@ jobs: - name: Upload test suite to Browserstack id: upload env: - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | ZIP_NAME="maestro_tests.zip" @@ -211,9 +211,9 @@ jobs: with: app_url: ${{ needs.build-android.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} - project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME }} - browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME }} - browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME_TESTS }} + browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} + browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing timeout: 1800 devices: | @@ -258,9 +258,9 @@ jobs: with: app_url: ${{ needs.build-ios.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} - project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME }} - browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME }} - browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME_TESTS }} + browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} + browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing timeout: 1800 devices: | From ea4303c98743991dbf770e4b016b577f877b5317 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:31:16 +0100 Subject: [PATCH 14/45] fix missing npm install step for build-ios --- .github/workflows/e2e-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 67549db..c575889 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -128,6 +128,11 @@ jobs: if: steps.cache-libnode.outputs.cache-hit != 'true' run: ./scripts/download-nodejs-mobile.sh + - name: Install npm dependencies + run: | + npm install --ignore-scripts + npx patch-package + - name: Build backend bundle run: npm run backend:build From 2ff9c2a0b443b0a4e82867b3e15c1d03c0e4ba85 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:33:05 +0100 Subject: [PATCH 15/45] fix e2e app setup step for build-ios --- .github/workflows/e2e-tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c575889..b92c22b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -136,9 +136,12 @@ jobs: - name: Build backend bundle run: npm run backend:build - - name: Expo prebuild + - name: Set up E2E app working-directory: apps/e2e - run: npx expo prebuild --platform ios + run: | + npm install --ignore-scripts + npx patch-package + npx expo prebuild --platform ios - name: Build .ipa working-directory: apps/e2e/ios From e8ab704d28b9b2387fdf023514d1b82adf8b5122 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:35:24 +0100 Subject: [PATCH 16/45] rename flow file --- .github/workflows/e2e-tests.yml | 2 +- maestro/{e2e.yml => e2e.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename maestro/{e2e.yml => e2e.yaml} (100%) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b92c22b..4d287ea 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -191,7 +191,7 @@ jobs: run: | ZIP_NAME="maestro_tests.zip" - zip -r "$ZIP_NAME" maestro/e2e.yml + zip -r "$ZIP_NAME" maestro/e2e.yaml ZIP_PATH="$(pwd)/$ZIP_NAME" diff --git a/maestro/e2e.yml b/maestro/e2e.yaml similarity index 100% rename from maestro/e2e.yml rename to maestro/e2e.yaml From 1762251db93fa107cdc3a6285a2c26718bda24d4 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:43:10 +0100 Subject: [PATCH 17/45] maybe fix -F curl usage --- .github/workflows/e2e-tests.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4d287ea..ac7cd2d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -85,11 +85,11 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | - APK_PATH="$(pwd)/apps/e2e/android/app/build/outputs/apk/release/app-release.apk" + APK_RELATIVE_PATH="apps/e2e/android/app/build/outputs/apk/release/app-release.apk" APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@$APK_PATH" | jq -r '.app_url') + -F "file=@$APK_RELATIVE_PATH" | jq -r '.app_url') echo "app_url=$APP_URL" >> $GITHUB_OUTPUT @@ -161,11 +161,11 @@ jobs: - name: Upload id: upload run: | - IPA_PATH="$(pwd)/apps/e2e/ios/corereactnativee2e.ipa" + IPA_RELATIVE_PATH="apps/e2e/ios/corereactnativee2e.ipa" APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@$IPA_PATH" | jq -r '.app_url') + -F "file=@$IPA_RELATIVE_PATH" | jq -r '.app_url') echo "app_url=$APP_URL" >> $GITHUB_OUTPUT @@ -193,11 +193,9 @@ jobs: zip -r "$ZIP_NAME" maestro/e2e.yaml - ZIP_PATH="$(pwd)/$ZIP_NAME" - TEST_SUITE_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ - -F "file=@$ZIP_PATH" | jq -r '.test_suite_url') + -F "file=@$ZIP_NAME" | jq -r '.test_suite_url') echo "test_suite_url=$TEST_SUITE_URL" >> $GITHUB_OUTPUT From 4c6bcd18be84d63fb8d4928c60037448b9b2b3d0 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:50:23 +0100 Subject: [PATCH 18/45] fix build-android --- .github/workflows/e2e-tests.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ac7cd2d..f14355d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,17 +55,16 @@ jobs: if: steps.cache-libnode.outputs.cache-hit != 'true' run: ./scripts/download-nodejs-mobile.sh + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Install npm dependencies run: | npm install --ignore-scripts npx patch-package - - name: Expo prebuild (generates apps/e2e/android) - working-directory: apps/e2e - run: npx expo prebuild --platform android --no-install - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + - name: Build backend bundle + run: npm run backend:build - name: Set up E2E app working-directory: apps/e2e From e5fe3ff6edb90f88048c22318dc6b413fff4035a Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 12:56:36 +0100 Subject: [PATCH 19/45] improve curl calls --- .github/workflows/e2e-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f14355d..8f90c4f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -86,7 +86,7 @@ jobs: run: | APK_RELATIVE_PATH="apps/e2e/android/app/build/outputs/apk/release/app-release.apk" - APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + APP_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ -F "file=@$APK_RELATIVE_PATH" | jq -r '.app_url') @@ -162,7 +162,7 @@ jobs: run: | IPA_RELATIVE_PATH="apps/e2e/ios/corereactnativee2e.ipa" - APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + APP_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ -F "file=@$IPA_RELATIVE_PATH" | jq -r '.app_url') @@ -192,7 +192,7 @@ jobs: zip -r "$ZIP_NAME" maestro/e2e.yaml - TEST_SUITE_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + TEST_SUITE_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ -F "file=@$ZIP_NAME" | jq -r '.test_suite_url') From 101e0ce600ee265a5e676f57fa8206f3c03430b5 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 13:01:35 +0100 Subject: [PATCH 20/45] fix upload step in build-ios --- .github/workflows/e2e-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8f90c4f..efd244b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -159,6 +159,9 @@ jobs: - name: Upload id: upload + env: + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | IPA_RELATIVE_PATH="apps/e2e/ios/corereactnativee2e.ipa" From 6254152e35e08d4676089ce7471b719fb8be74e3 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 13:02:50 +0100 Subject: [PATCH 21/45] add commented out needs spec to upload-test-suite --- .github/workflows/e2e-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index efd244b..967ac1f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -174,6 +174,8 @@ jobs: upload-test-suite: if: ${{ github.event.pull_request.draft == false }} name: Upload test suite + # TODO: Enable when we know this job works + # needs: [build-android, build-ios] runs-on: ubuntu-latest permissions: contents: read From 3dbcd6c20088cea0cc33ca900f06799fe8b9261d Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 13:09:41 +0100 Subject: [PATCH 22/45] rename zip file used for upload --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 967ac1f..fc50f59 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -193,7 +193,7 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | - ZIP_NAME="maestro_tests.zip" + ZIP_NAME="flows.zip" zip -r "$ZIP_NAME" maestro/e2e.yaml From 93c6f6183d73750f29536d3f34486354b19cecdc Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 13:58:45 +0100 Subject: [PATCH 23/45] curl fixes --- .github/actions/run-browserstack-maestro/action.yml | 4 ++-- .github/workflows/e2e-tests.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index a39db49..511fe72 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -54,7 +54,7 @@ runs: --arg project "$PROJECT_NAME" \ '{app: $app, testSuite: $suite, devices: $devices, project: $project, deviceLogs: true, maestroVersion: "latest"}') - BUILD_ID=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + BUILD_ID=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ -H "Content-Type: application/json" \ -d "$PAYLOAD" \ @@ -76,7 +76,7 @@ runs: ELAPSED=0 while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do - RESPONSE=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + RESPONSE=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ "https://api-cloud.browserstack.com/app-automate/maestro/v2/builds/$BUILD_ID") STATUS=$(echo "$RESPONSE" | jq -r '.status') diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fc50f59..eaf77e4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -86,7 +86,7 @@ jobs: run: | APK_RELATIVE_PATH="apps/e2e/android/app/build/outputs/apk/release/app-release.apk" - APP_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + APP_URL=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ -F "file=@$APK_RELATIVE_PATH" | jq -r '.app_url') @@ -165,7 +165,7 @@ jobs: run: | IPA_RELATIVE_PATH="apps/e2e/ios/corereactnativee2e.ipa" - APP_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + APP_URL=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ -F "file=@$IPA_RELATIVE_PATH" | jq -r '.app_url') @@ -197,7 +197,7 @@ jobs: zip -r "$ZIP_NAME" maestro/e2e.yaml - TEST_SUITE_URL=$(curl --fail --show-error -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + TEST_SUITE_URL=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/test-suite" \ -F "file=@$ZIP_NAME" | jq -r '.test_suite_url') From b01826bfd364bac1a80eac71dc989c24d1fd8f9c Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 13:59:24 +0100 Subject: [PATCH 24/45] fix caching of nodejs-mobile for ios --- .github/workflows/e2e-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index eaf77e4..ea2c974 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -114,14 +114,14 @@ jobs: node-version-file: package.json - name: Cache nodejs-mobile binaries - id: cache-libnode + id: cache-ios-framework uses: actions/cache@v4 with: - path: android/libnode + path: ios/NodeMobile.xcframework # hashFiles(...) folds the BASE_URL (defined inside the # download script) into the cache key, so any swap of the # source repo automatically invalidates stale caches. - key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-android-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} + key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-ios-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} - name: Download nodejs-mobile binaries if: steps.cache-libnode.outputs.cache-hit != 'true' From 0d72bc1568efb3fa635540c120de05c70c3c899d Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 14:09:02 +0100 Subject: [PATCH 25/45] minor fixup --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ea2c974..c3e0e6c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -84,7 +84,7 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | - APK_RELATIVE_PATH="apps/e2e/android/app/build/outputs/apk/release/app-release.apk" + APK_RELATIVE_PATH="./apps/e2e/android/app/build/outputs/apk/release/app-release.apk" APP_URL=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ @@ -163,7 +163,7 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} run: | - IPA_RELATIVE_PATH="apps/e2e/ios/corereactnativee2e.ipa" + IPA_RELATIVE_PATH="./apps/e2e/ios/corereactnativee2e.ipa" APP_URL=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ From 44335e7df19f530e01a62fe7019d0158cbf14e12 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 14:28:46 +0100 Subject: [PATCH 26/45] limit devices used due to browserstack plan limitations --- .github/workflows/e2e-tests.yml | 37 +++++++++++---------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c3e0e6c..98b8100 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -226,28 +226,16 @@ jobs: browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing timeout: 1800 - devices: | + devices: >- [ - "Google Pixel 5-11.0", - "Google Pixel 9-16.0", - "Huawei P30-9.0", - "Huawei Nova 11 SE-12.0", - "Motorola Moto G9 Play-10.0", - "Motorola Moto G71 5G-11.0", - "OnePlus 9-11.0", - "OnePlus 11R-13.0", - "OnePlus 12R-14.0", - "OnePlus 13R-15.0", - "Oppo Reno 3 Pro-10.0", - "Oppo Reno 6-11.0", - "Samsung Galaxy Note 9-8.1", - "Samsung Galaxy S10-9.0", - "Samsung Galaxy A51-10.0", - "Samsung Galaxy S23-13.0", - "Vivo V21-11.0", - "Vivo Y21-11.0", - "Xiaomi Redmi Note 9-10.0", - "Xiaomi Redmi Note 11-11.0" + "Google Pixel 9-16.0", + "Huawei Nova 11 SE-12.0", + "Motorola Moto G71 5G-11.0", + "OnePlus 13R-15.0", + "Oppo Reno 6-11.0", + "Samsung Galaxy Note 9-8.1", + "Vivo Y21-11.0", + "Xiaomi Redmi Note 11-11.0" ] test-ios: @@ -273,9 +261,8 @@ jobs: browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing timeout: 1800 - devices: | + devices: >- [ - "iPhone 13-15", - "iPhone 15-17", - "iPhone 17-26" + "iPhone 13-15", + "iPhone 17-26" ] From f9cebcf4a96cfad0abbeb8d3519a5581a5090e11 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 14:46:21 +0100 Subject: [PATCH 27/45] browserstack project adjustements --- .github/actions/run-browserstack-maestro/action.yml | 8 ++++---- .github/workflows/e2e-tests.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index 511fe72..aa2c68f 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -12,8 +12,8 @@ inputs: devices: description: JSON array of device strings required: true - project_name: - description: BrowserStack project name + browserstack_project: + description: BrowserStack project required: true browserstack_username: description: BrowserStack username @@ -42,7 +42,7 @@ runs: APP_URL: ${{ inputs.app_url }} TEST_SUITE_URL: ${{ inputs.test_suite_url }} DEVICES: ${{ inputs.devices }} - PROJECT_NAME: ${{ inputs.project_name }} + BROWSERSTACK_PROJECT: ${{ inputs.browserstack_project }} BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} shell: bash @@ -51,7 +51,7 @@ runs: --arg app "$APP_URL" \ --arg suite "$TEST_SUITE_URL" \ --argjson devices "$DEVICES" \ - --arg project "$PROJECT_NAME" \ + --arg project "$BROWSERSTACK_PROJECT" \ '{app: $app, testSuite: $suite, devices: $devices, project: $project, deviceLogs: true, maestroVersion: "latest"}') BUILD_ID=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 98b8100..702c3d9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -221,7 +221,7 @@ jobs: with: app_url: ${{ needs.build-android.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} - project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME_TESTS }} + browserstack_project: ${{ vars.BROWSERSTACK_PROJECT_TESTS }} browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing @@ -256,7 +256,7 @@ jobs: with: app_url: ${{ needs.build-ios.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} - project_name: ${{ vars.BROWSERSTACK_PROJECT_NAME_TESTS }} + browserstack_project: ${{ vars.BROWSERSTACK_PROJECT_TESTS }} browserstack_username: ${{ secrets.BROWSERSTACK_USERNAME_TESTS }} browserstack_access_key: ${{ secrets.BROWSERSTACK_ACCESS_KEY_TESTS }} # TODO: Adjust based on actual timing From 490d6f7ffa323a40710e12f90a06b5f67330e75f Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 15:26:18 +0100 Subject: [PATCH 28/45] fix url used when executing build --- .../run-browserstack-maestro/action.yml | 21 +++++++++++++++---- .github/workflows/e2e-tests.yml | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index aa2c68f..b8b5868 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -3,6 +3,9 @@ name: Run BrowserStack Maestro Tests description: Triggers a BrowserStack Maestro build and polls until completion inputs: + platform: + description: Platform to run tests on (android or ios) + required: true app_url: description: BrowserStack app URL (bs://...) required: true @@ -30,11 +33,20 @@ runs: using: composite steps: - name: Validate inputs - if: ${{ fromJson(inputs.timeout) < 30 }} + env: + PLATFORM: ${{ inputs.platform }} + TIMEOUT: ${{ fromJson(inputs.timeout) }} shell: bash run: | - echo "timeout must be at least 30 seconds" - exit 1 + if [[ "$PLATFORM" != "android" && "$PLATFORM" != "ios" ]]; then + echo "platform must be android or ios" + exit 1 + fi + + if [[ "$TIMEOUT" -lt 30 ]]; then + echo "timeout must be at least 30 seconds" + exit 1 + fi - name: Trigger build id: trigger @@ -45,6 +57,7 @@ runs: BROWSERSTACK_PROJECT: ${{ inputs.browserstack_project }} BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} + PLATFORM: ${{ inputs.platform }} shell: bash run: | PAYLOAD=$(jq -n \ @@ -55,7 +68,7 @@ runs: '{app: $app, testSuite: $suite, devices: $devices, project: $project, deviceLogs: true, maestroVersion: "latest"}') BUILD_ID=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ - -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/build" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/$PLATFORM/build" \ -H "Content-Type: application/json" \ -d "$PAYLOAD" \ | jq -r '.build_id') diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 702c3d9..90057cd 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -219,6 +219,7 @@ jobs: - uses: ./.github/actions/run-browserstack-maestro with: + platform: android app_url: ${{ needs.build-android.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} browserstack_project: ${{ vars.BROWSERSTACK_PROJECT_TESTS }} @@ -254,6 +255,7 @@ jobs: - uses: ./.github/actions/run-browserstack-maestro with: + platform: android app_url: ${{ needs.build-ios.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} browserstack_project: ${{ vars.BROWSERSTACK_PROJECT_TESTS }} From ec186be85c9d7f5c32eb19b8494d0067618b3c9f Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 15:41:04 +0100 Subject: [PATCH 29/45] fix dumb mistake --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 90057cd..4de4978 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -255,7 +255,7 @@ jobs: - uses: ./.github/actions/run-browserstack-maestro with: - platform: android + platform: ios app_url: ${{ needs.build-ios.outputs.app_url }} test_suite_url: ${{ needs.upload-test-suite.outputs.test_suite_url }} browserstack_project: ${{ vars.BROWSERSTACK_PROJECT_TESTS }} From fd2ee6948bdaacaa1571f02a0ea26674224d3559 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 15:44:05 +0100 Subject: [PATCH 30/45] update devices --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4de4978..95618e7 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -265,6 +265,6 @@ jobs: timeout: 1800 devices: >- [ - "iPhone 13-15", + "iPhone 15-17", "iPhone 17-26" ] From 557529eee759a141af57a255a16c074c99b7b66a Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 15:58:01 +0100 Subject: [PATCH 31/45] increase timeout for flow in CI --- .github/actions/run-browserstack-maestro/action.yml | 12 +++++++++++- maestro/e2e.yaml | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index b8b5868..c35e41e 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -58,6 +58,7 @@ runs: BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} PLATFORM: ${{ inputs.platform }} + TIMEOUT: 60000 shell: bash run: | PAYLOAD=$(jq -n \ @@ -65,7 +66,16 @@ runs: --arg suite "$TEST_SUITE_URL" \ --argjson devices "$DEVICES" \ --arg project "$BROWSERSTACK_PROJECT" \ - '{app: $app, testSuite: $suite, devices: $devices, project: $project, deviceLogs: true, maestroVersion: "latest"}') + '{ + app: $app, + testSuite: $suite, + devices: $devices, + project: $project, + setEnvVariables: { "TIMEOUT": $TIMEOUT }, + deviceLogs: true, + maestroVersion: "latest" + }' + ) BUILD_ID=$(curl --fail --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ -X POST "https://api-cloud.browserstack.com/app-automate/maestro/v2/$PLATFORM/build" \ diff --git a/maestro/e2e.yaml b/maestro/e2e.yaml index e864a84..8f50998 100644 --- a/maestro/e2e.yaml +++ b/maestro/e2e.yaml @@ -1,4 +1,6 @@ appId: com.comapeo.core.e2e +env: + TIMEOUT: ${TIMEOUT || 10000} --- - launchApp: clearState: true @@ -8,4 +10,4 @@ appId: com.comapeo.core.e2e - extendedWaitUntil: visible: id: "all-tests-passed" - timeout: 10000 + timeout: ${TIMEOUT} From f9a6b4765badd30151c0f1296798e0a5aba52502 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 16:14:22 +0100 Subject: [PATCH 32/45] fix timeout interpolation --- .github/actions/run-browserstack-maestro/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index c35e41e..fb04d56 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -66,12 +66,13 @@ runs: --arg suite "$TEST_SUITE_URL" \ --argjson devices "$DEVICES" \ --arg project "$BROWSERSTACK_PROJECT" \ + --argjson timeout "$TIMEOUT" \ '{ app: $app, testSuite: $suite, devices: $devices, project: $project, - setEnvVariables: { "TIMEOUT": $TIMEOUT }, + setEnvVariables: { "TIMEOUT": $timeout }, deviceLogs: true, maestroVersion: "latest" }' From 4b7fe32983c452693f2b25e20277e06564c7ab76 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 17:29:30 +0100 Subject: [PATCH 33/45] stick with (larger) hardcoded timeout for now --- .github/actions/run-browserstack-maestro/action.yml | 3 --- maestro/e2e.yaml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index fb04d56..8725865 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -58,7 +58,6 @@ runs: BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} PLATFORM: ${{ inputs.platform }} - TIMEOUT: 60000 shell: bash run: | PAYLOAD=$(jq -n \ @@ -66,13 +65,11 @@ runs: --arg suite "$TEST_SUITE_URL" \ --argjson devices "$DEVICES" \ --arg project "$BROWSERSTACK_PROJECT" \ - --argjson timeout "$TIMEOUT" \ '{ app: $app, testSuite: $suite, devices: $devices, project: $project, - setEnvVariables: { "TIMEOUT": $timeout }, deviceLogs: true, maestroVersion: "latest" }' diff --git a/maestro/e2e.yaml b/maestro/e2e.yaml index 8f50998..9343edb 100644 --- a/maestro/e2e.yaml +++ b/maestro/e2e.yaml @@ -1,6 +1,6 @@ appId: com.comapeo.core.e2e env: - TIMEOUT: ${TIMEOUT || 10000} + TIMEOUT: 30000 --- - launchApp: clearState: true From 132f4146bbd9da6eff8ba46d39391d77cc9abcc1 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 7 May 2026 17:53:25 +0100 Subject: [PATCH 34/45] remove startRecording command --- .gitignore | 3 --- maestro/e2e.yaml | 2 -- 2 files changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index e3687e8..cdfe0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,3 @@ build/ # eslint .eslintcache - -# Maestro artifacts -maestro/recordings/ diff --git a/maestro/e2e.yaml b/maestro/e2e.yaml index 9343edb..9ac2754 100644 --- a/maestro/e2e.yaml +++ b/maestro/e2e.yaml @@ -4,8 +4,6 @@ env: --- - launchApp: clearState: true -- startRecording: - path: "maestro/recordings/e2e" - tapOn: "Run tests" - extendedWaitUntil: visible: From f4d47e8896333d9e34e3b2932cbe1f452af28b5a Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Tue, 19 May 2026 10:58:30 +0100 Subject: [PATCH 35/45] stop browserstack build/tests on workflow cancel --- .../actions/run-browserstack-maestro/action.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/actions/run-browserstack-maestro/action.yml b/.github/actions/run-browserstack-maestro/action.yml index 8725865..b317c2c 100644 --- a/.github/actions/run-browserstack-maestro/action.yml +++ b/.github/actions/run-browserstack-maestro/action.yml @@ -114,3 +114,17 @@ runs: echo "Timed out after ${MAX_WAIT}s waiting for build $BUILD_ID" exit 1 + + - name: Stop BrowserStack build on cancel + if: cancelled() && steps.trigger.outputs.build_id != '' + env: + BUILD_ID: ${{ steps.trigger.outputs.build_id }} + BROWSERSTACK_USERNAME: ${{ inputs.browserstack_username }} + BROWSERSTACK_ACCESS_KEY: ${{ inputs.browserstack_access_key }} + shell: bash + run: | + [[ "$BUILD_ID" == "null" ]] && exit 0 + echo "Stopping BrowserStack build $BUILD_ID" + curl --show-error -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/maestro/builds/$BUILD_ID/stop" \ + -H "Content-Type: application/json" || true From 85451200d2cfb3bcbba28057e4830ac3ce0dca9e Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Tue, 19 May 2026 10:59:12 +0100 Subject: [PATCH 36/45] fix cache step ID --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 95618e7..fc87139 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -124,7 +124,7 @@ jobs: key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-ios-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} - name: Download nodejs-mobile binaries - if: steps.cache-libnode.outputs.cache-hit != 'true' + if: steps.cache-ios-framework.outputs.cache-hit != 'true' run: ./scripts/download-nodejs-mobile.sh - name: Install npm dependencies From 2430a78c9362a19c1cb5d63b19b4a98a535ad85c Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Tue, 19 May 2026 12:46:35 +0100 Subject: [PATCH 37/45] fix(e2e): raise jasmine timeout, close projects, surface done signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set jasmine.DEFAULT_TIMEOUT_INTERVAL to 60s — the 5s default was tripping the CRUD tests that fan out 100 IPC create()/delete() calls on slower BrowserStack devices. - Track every opened project in project-crud and close them in afterEach so listeners don't accumulate across tests (was producing MaxListenersExceededWarning on SocketMessagePort / LocalPeers and slowing later tests). - Add an `all-tests-done` testID alongside `all-tests-passed` / `all-tests-failed` so Maestro can fail fast on a failing run instead of waiting for the full timeout. - Bump Maestro `extendedWaitUntil` to 5 min and assert pass after done. - Log spec start / pass / fail (with stacks) via console.log so they appear under the ReactNativeJS tag in logcat / device logs. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/e2e/src/TestRunner.tsx | 50 ++++++++++++++++++++++++++++-- apps/e2e/src/tests/project-crud.ts | 35 ++++++++++++++++----- apps/e2e/src/tests/utils.ts | 8 ++++- maestro/e2e.yaml | 9 ++++-- 4 files changed, 87 insertions(+), 15 deletions(-) diff --git a/apps/e2e/src/TestRunner.tsx b/apps/e2e/src/TestRunner.tsx index 7f706a0..a6d87ca 100644 --- a/apps/e2e/src/TestRunner.tsx +++ b/apps/e2e/src/TestRunner.tsx @@ -18,6 +18,9 @@ type TestState = | { status: 'idle' | 'pending'; results: Array } | { status: 'done'; info: JasmineDoneInfo; results: Array } +// Default of 5s is too short for IPC-heavy tests on slow CI devices. +const DEFAULT_TIMEOUT_INTERVAL_MS = 60_000 + export function TestRunner() { const [testState, setTestState] = useState({ status: 'idle', @@ -34,9 +37,11 @@ export function TestRunner() { jasmineEnv.addReporter({ jasmineStarted: () => { + console.log('[e2e] jasmine started') setTestState({ status: 'pending', results: [] }) }, jasmineDone: (info) => { + console.log(`[e2e] jasmine done: ${info.overallStatus}`) setTestState((prev) => { if (prev.status === 'done') { throw new Error( @@ -51,9 +56,25 @@ export function TestRunner() { } }) }, + specStarted: (result) => { + console.log(`[e2e] spec started: ${result.fullName}`) + }, specDone: (result) => { const describeText = result.fullName.replaceAll(result.description, '') + if (result.status === 'passed') { + console.log(`[e2e] PASS: ${result.fullName}`) + } else { + console.log( + `[e2e] FAIL: ${result.fullName} — ${result.failedExpectations + .map((e) => e.message) + .join(' | ')}`, + ) + for (const err of result.failedExpectations) { + if (err.stack) console.log(`[e2e] stack: ${err.stack}`) + } + } + setTestState((prev) => { if (prev.status === 'done') { throw new Error( @@ -82,12 +103,24 @@ export function TestRunner() { }, }) - const { describe, it, expect, expectAsync, jasmine } = + const { describe, it, expect, expectAsync, jasmine, beforeEach, afterEach } = jasmineRequire.interface(jasmineCore, jasmineEnv) + jasmine.DEFAULT_TIMEOUT_INTERVAL = DEFAULT_TIMEOUT_INTERVAL_MS + + const ctx = { + describe, + it, + expect, + expectAsync, + jasmine, + beforeEach, + afterEach, + } + // 👇 Register tests here! - basicTest({ describe, it, expect, expectAsync, jasmine }) - projectCrudTest({ describe, it, expect, expectAsync, jasmine }) + basicTest(ctx) + projectCrudTest(ctx) await jasmineEnv.execute() } @@ -106,10 +139,21 @@ export function TestRunner() { {`${testState.status === 'pending' ? 'Pending' : 'Done'}: ${testState.results.filter((r) => r.passed).length} out of ${testState.results.length} tests passed`} + {testState.status === 'done' ? ( + Done. + ) : null} + {testState.status === 'done' && testState.info.overallStatus === 'passed' ? ( All tests passed! ) : null} + + {testState.status === 'done' && + testState.info.overallStatus !== 'passed' ? ( + + {`Tests failed (${testState.info.overallStatus}).`} + + ) : null} ) : null} diff --git a/apps/e2e/src/tests/project-crud.ts b/apps/e2e/src/tests/project-crud.ts index fe61ec1..6fa0a9a 100644 --- a/apps/e2e/src/tests/project-crud.ts +++ b/apps/e2e/src/tests/project-crud.ts @@ -38,9 +38,28 @@ export function test({ expectAsync, it, jasmine, + afterEach, }: TestContext) { const CREATE_COUNT = 100 + const openProjects = new Set() + + async function openProject(projectId: string): Promise { + const project = await comapeo.getProject(projectId) + openProjects.add(project) + return project + } + + afterEach(async () => { + const projects = [...openProjects] + openProjects.clear() + // Close in afterEach to avoid leaking listeners across tests (otherwise + // EventEmitter MaxListenersExceeded fires and later tests slow down). + await Promise.all( + projects.map((p) => p.close().catch(() => undefined)), + ) + }) + const FIXTURES: Array< | FieldValue | ObservationValue @@ -105,7 +124,7 @@ export function test({ it(`create and read (${schemaName})`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const updates: Array = [] project[schemaName].on('updated-docs', (docs) => updates.push(...docs)) const written = await createWithMockData( @@ -132,7 +151,7 @@ export function test({ it(`update (${schemaName})`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const written = await create(project, value) const updateValue = getUpdateFixture(value) @@ -163,7 +182,7 @@ export function test({ it(`getMany (${schemaName})`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const written = await createWithMockData( project, schemaName, @@ -198,7 +217,7 @@ export function test({ it(`create, close and then create, update (${schemaName})`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const values = new Array(5).fill(null).map(() => { return getUpdateFixture(value) }) @@ -237,7 +256,7 @@ export function test({ it(`create, read, close, re-open, read (${schemaName})`, async () => { const projectId = await comapeo.createProject() - let project = await comapeo.getProject(projectId) + let project = await openProject(projectId) const values = new Array(5).fill(null).map(() => { return getUpdateFixture(value) @@ -254,7 +273,7 @@ export function test({ await project.close() // re-open project - project = await comapeo.getProject(projectId) + project = await openProject(projectId) const many2 = await project[schemaName].getMany() const manyValues2 = many2.map((doc) => valueOf(doc)) @@ -267,7 +286,7 @@ export function test({ it(`create and delete (${schemaName})`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const written = await createWithMockData( project, schemaName, @@ -289,7 +308,7 @@ export function test({ it(`delete forks ${schemaName}`, async () => { const projectId = await comapeo.createProject() - const project = await comapeo.getProject(projectId) + const project = await openProject(projectId) const written = await create(project, value) const updateValue = getUpdateFixture(value) const updatedFork1 = await update( diff --git a/apps/e2e/src/tests/utils.ts b/apps/e2e/src/tests/utils.ts index e1dd86a..cbd01f4 100644 --- a/apps/e2e/src/tests/utils.ts +++ b/apps/e2e/src/tests/utils.ts @@ -3,7 +3,13 @@ import { JasmineInterface } from 'jasmine-core/lib/jasmine-core/jasmine' export type TestContext = Pick< JasmineInterface, - 'describe' | 'it' | 'expect' | 'expectAsync' | 'jasmine' + | 'describe' + | 'it' + | 'expect' + | 'expectAsync' + | 'jasmine' + | 'beforeEach' + | 'afterEach' > export function sortBy(arr: Array, key: keyof T) { diff --git a/maestro/e2e.yaml b/maestro/e2e.yaml index 9ac2754..dc50024 100644 --- a/maestro/e2e.yaml +++ b/maestro/e2e.yaml @@ -1,11 +1,14 @@ appId: com.comapeo.core.e2e env: - TIMEOUT: 30000 + TIMEOUT: 300000 --- - launchApp: clearState: true - tapOn: "Run tests" - extendedWaitUntil: - visible: - id: "all-tests-passed" + visible: + id: "all-tests-done" timeout: ${TIMEOUT} +- takeScreenshot: results +- assertVisible: + id: "all-tests-passed" From 749a7919dfae577760e9cc90b10c7cd8b28134fb Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Tue, 19 May 2026 22:08:56 +0100 Subject: [PATCH 38/45] ci(e2e): drop --ignore-scripts so root prepare runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root package.json `prepare` script (`expo-module prepare`, plus `scripts/check-sentry-cocoa-pin.mjs`) needs to run before the e2e app prebuilds, otherwise the module's native scaffolding isn't regenerated and the iOS Pods build fails to resolve `SentrySDK.startTransaction` through the `Sentry/HybridSDK` module. The `iOS Tests` workflow that builds `apps/example` runs `npm install` (no `--ignore-scripts`) and passes — match that. `patch-package` is already wired up as `postinstall` in both package.jsons, so the explicit `npx patch-package` calls are redundant once scripts run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/e2e-tests.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fc87139..3e6680b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -59,9 +59,7 @@ jobs: uses: gradle/actions/setup-gradle@v4 - name: Install npm dependencies - run: | - npm install --ignore-scripts - npx patch-package + run: npm install - name: Build backend bundle run: npm run backend:build @@ -69,8 +67,7 @@ jobs: - name: Set up E2E app working-directory: apps/e2e run: | - npm install --ignore-scripts - npx patch-package + npm install npx expo prebuild --platform android --no-install - name: Build APK @@ -128,9 +125,7 @@ jobs: run: ./scripts/download-nodejs-mobile.sh - name: Install npm dependencies - run: | - npm install --ignore-scripts - npx patch-package + run: npm install - name: Build backend bundle run: npm run backend:build @@ -138,8 +133,7 @@ jobs: - name: Set up E2E app working-directory: apps/e2e run: | - npm install --ignore-scripts - npx patch-package + npm install npx expo prebuild --platform ios - name: Build .ipa From d532088dd92510ec2753ab8f1e2beb0700a70fce Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 13:53:37 +0100 Subject: [PATCH 39/45] fix: fix Android 8.1 crash by overriding sodium-native v5 Seems like the older sodium-native (libsodium) was crashing Android v8.1 --- backend/package-lock.json | 247 ++------------------------------------ backend/package.json | 4 +- 2 files changed, 13 insertions(+), 238 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index b154cb9..d67f200 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1313,37 +1313,6 @@ "unslab": "^1.3.0" } }, - "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", - "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", - "license": "MIT", - "dependencies": { - "bare-assert": "^1.2.0", - "require-addon": "^1.1.0", - "which-runtime": "^1.2.1" - }, - "engines": { - "bare": ">=1.16.0" - } - }, - "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", - "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", - "license": "MIT", - "dependencies": { - "sodium-native": "^5.0.1" - }, - "peerDependencies": { - "sodium-javascript": "~0.8.0" - }, - "peerDependenciesMeta": { - "sodium-javascript": { - "optional": true - } - } - }, "node_modules/@inquirer/checkbox": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", @@ -3365,19 +3334,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, "node_modules/@types/readable-stream": { "version": "4.0.23", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", @@ -6754,37 +6710,6 @@ "sodium-universal": "^5.0.0" } }, - "node_modules/noise-curve-ed/node_modules/sodium-native": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", - "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", - "license": "MIT", - "dependencies": { - "bare-assert": "^1.2.0", - "require-addon": "^1.1.0", - "which-runtime": "^1.2.1" - }, - "engines": { - "bare": ">=1.16.0" - } - }, - "node_modules/noise-curve-ed/node_modules/sodium-universal": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", - "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", - "license": "MIT", - "dependencies": { - "sodium-native": "^5.0.1" - }, - "peerDependencies": { - "sodium-javascript": "~0.8.0" - }, - "peerDependenciesMeta": { - "sodium-javascript": { - "optional": true - } - } - }, "node_modules/noise-handshake": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz", @@ -6796,37 +6721,6 @@ "sodium-universal": "^5.0.0" } }, - "node_modules/noise-handshake/node_modules/sodium-native": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", - "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", - "license": "MIT", - "dependencies": { - "bare-assert": "^1.2.0", - "require-addon": "^1.1.0", - "which-runtime": "^1.2.1" - }, - "engines": { - "bare": ">=1.16.0" - } - }, - "node_modules/noise-handshake/node_modules/sodium-universal": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", - "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", - "license": "MIT", - "dependencies": { - "sodium-native": "^5.0.1" - }, - "peerDependencies": { - "sodium-javascript": "~0.8.0" - }, - "peerDependenciesMeta": { - "sodium-javascript": { - "optional": true - } - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7336,43 +7230,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7463,53 +7320,6 @@ "integrity": "sha512-13Lg5S4fkF6onHsIeITWBnrPYhPe6iqdZCGJ8K+RAlL3qvGptPuWR4aQtULmbkGW2/XzXxWoHKlWsYRAvfcLnA==", "license": "MIT" }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -8433,25 +8243,6 @@ "license": "MIT" }, "node_modules/sodium-native": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", - "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", - "license": "MIT", - "dependencies": { - "require-addon": "^1.1.0" - } - }, - "node_modules/sodium-secretstream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz", - "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==", - "license": "MIT", - "dependencies": { - "b4a": "^1.1.1", - "sodium-universal": "^5.0.0" - } - }, - "node_modules/sodium-secretstream/node_modules/sodium-native": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", @@ -8465,30 +8256,23 @@ "bare": ">=1.16.0" } }, - "node_modules/sodium-secretstream/node_modules/sodium-universal": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", - "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", + "node_modules/sodium-secretstream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz", + "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==", "license": "MIT", "dependencies": { - "sodium-native": "^5.0.1" - }, - "peerDependencies": { - "sodium-javascript": "~0.8.0" - }, - "peerDependenciesMeta": { - "sodium-javascript": { - "optional": true - } + "b4a": "^1.1.1", + "sodium-universal": "^5.0.0" } }, "node_modules/sodium-universal": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz", - "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", + "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", "license": "MIT", "dependencies": { - "sodium-native": "^4.0.0" + "sodium-native": "^5.0.1" }, "peerDependencies": { "sodium-javascript": "~0.8.0" @@ -9470,17 +9254,6 @@ "url": "https://opencollective.com/xstate" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 1089695..1174d58 100644 --- a/backend/package.json +++ b/backend/package.json @@ -54,6 +54,8 @@ "typescript": "5.9.3" }, "overrides": { - "require-addon": "1.1.0" + "require-addon": "1.1.0", + "sodium-native": "5.1.0", + "sodium-universal": "5.0.1" } } From 80127895dd6577b4d40756e2dd219134da359ce2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:12:26 +0000 Subject: [PATCH 40/45] fix(e2e): add BUILD_LIBRARY_FOR_DISTRIBUTION to Sentry pods for Xcode 26 compat --- apps/e2e/app.json | 5 +- .../with-sentry-build-for-distribution.js | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 apps/e2e/plugins/with-sentry-build-for-distribution.js diff --git a/apps/e2e/app.json b/apps/e2e/app.json index fdd77ad..99fe85b 100644 --- a/apps/e2e/app.json +++ b/apps/e2e/app.json @@ -22,6 +22,9 @@ }, "package": "com.comapeo.core.e2e" }, - "plugins": [["expo-dev-client", { "launchMode": "most-recent" }]] + "plugins": [ + ["expo-dev-client", { "launchMode": "most-recent" }], + "./plugins/with-sentry-build-for-distribution" + ] } } diff --git a/apps/e2e/plugins/with-sentry-build-for-distribution.js b/apps/e2e/plugins/with-sentry-build-for-distribution.js new file mode 100644 index 0000000..f498639 --- /dev/null +++ b/apps/e2e/plugins/with-sentry-build-for-distribution.js @@ -0,0 +1,69 @@ +// E2e-app-only Expo config plugin. NOT part of the public +// @comapeo/core-react-native API. +// +// Workaround for sentry-cocoa issue #7950: under Xcode 26's Swift 6 toolchain, +// `@_implementationOnly import _SentryPrivate` in SentrySDK.swift prevents the +// Swift module interface from exporting `startTransaction` methods when a pod is +// built without BUILD_LIBRARY_FOR_DISTRIBUTION = YES. The symptom is a compile +// error "type 'SentrySDK' has no member 'startTransaction'" in any Swift file +// that calls that API (SentryNativeBridge.swift in our case). +// +// The fix is to set BUILD_LIBRARY_FOR_DISTRIBUTION = YES on all Sentry pod +// targets so that the Swift compiler generates a stable .swiftinterface text +// file that properly exports all public methods. We do this via a CocoaPods +// post_install hook injected idempotently into the generated Podfile. +// +// References: +// https://github.com/getsentry/sentry-cocoa/issues/7950 +const path = require('path'); +const fs = require('fs'); +const { withDangerousMod } = require('@expo/config-plugins'); +const { + mergeContents, +} = require('@expo/config-plugins/build/utils/generateCode'); + +const HOOK = `\ +post_install do |installer| + installer.pods_project.targets.each do |target| + next unless target.name.start_with?('Sentry') + + target.build_configurations.each do |config| + config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + end + end +end`; + +function withSentryBuildForDistribution(config) { + return withDangerousMod(config, [ + 'ios', + async (cfg) => { + const iosDir = path.join(cfg.modRequest.projectRoot, 'ios'); + patchPodfile(iosDir); + return cfg; + }, + ]); +} + +function patchPodfile(iosDir) { + const podfilePath = path.join(iosDir, 'Podfile'); + const podfile = fs.readFileSync(podfilePath, 'utf8'); + + // Insert our hook *before* the existing post_install block so that React + // Native's own post_install (react_native_post_install, etc.) still runs + // afterwards. mergeContents uses the tagged marker comments to make the + // injection idempotent across repeated `expo prebuild` runs. + const result = mergeContents({ + tag: 'with-sentry-build-for-distribution', + src: podfile, + newSrc: HOOK, + anchor: /^(\s*)post_install do \|installer\|/m, + offset: 0, + comment: '#', + }); + + if (result.didMerge) { + fs.writeFileSync(podfilePath, result.contents); + } +} + +module.exports = withSentryBuildForDistribution; From 8155aa2bc7a978b5ec9211612a706640daa3c231 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 16:10:40 +0100 Subject: [PATCH 41/45] Revert "fix(e2e): add BUILD_LIBRARY_FOR_DISTRIBUTION to Sentry pods for Xcode 26 compat" This reverts commit 80127895dd6577b4d40756e2dd219134da359ce2. --- apps/e2e/app.json | 5 +- .../with-sentry-build-for-distribution.js | 69 ------------------- 2 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 apps/e2e/plugins/with-sentry-build-for-distribution.js diff --git a/apps/e2e/app.json b/apps/e2e/app.json index 99fe85b..fdd77ad 100644 --- a/apps/e2e/app.json +++ b/apps/e2e/app.json @@ -22,9 +22,6 @@ }, "package": "com.comapeo.core.e2e" }, - "plugins": [ - ["expo-dev-client", { "launchMode": "most-recent" }], - "./plugins/with-sentry-build-for-distribution" - ] + "plugins": [["expo-dev-client", { "launchMode": "most-recent" }]] } } diff --git a/apps/e2e/plugins/with-sentry-build-for-distribution.js b/apps/e2e/plugins/with-sentry-build-for-distribution.js deleted file mode 100644 index f498639..0000000 --- a/apps/e2e/plugins/with-sentry-build-for-distribution.js +++ /dev/null @@ -1,69 +0,0 @@ -// E2e-app-only Expo config plugin. NOT part of the public -// @comapeo/core-react-native API. -// -// Workaround for sentry-cocoa issue #7950: under Xcode 26's Swift 6 toolchain, -// `@_implementationOnly import _SentryPrivate` in SentrySDK.swift prevents the -// Swift module interface from exporting `startTransaction` methods when a pod is -// built without BUILD_LIBRARY_FOR_DISTRIBUTION = YES. The symptom is a compile -// error "type 'SentrySDK' has no member 'startTransaction'" in any Swift file -// that calls that API (SentryNativeBridge.swift in our case). -// -// The fix is to set BUILD_LIBRARY_FOR_DISTRIBUTION = YES on all Sentry pod -// targets so that the Swift compiler generates a stable .swiftinterface text -// file that properly exports all public methods. We do this via a CocoaPods -// post_install hook injected idempotently into the generated Podfile. -// -// References: -// https://github.com/getsentry/sentry-cocoa/issues/7950 -const path = require('path'); -const fs = require('fs'); -const { withDangerousMod } = require('@expo/config-plugins'); -const { - mergeContents, -} = require('@expo/config-plugins/build/utils/generateCode'); - -const HOOK = `\ -post_install do |installer| - installer.pods_project.targets.each do |target| - next unless target.name.start_with?('Sentry') - - target.build_configurations.each do |config| - config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' - end - end -end`; - -function withSentryBuildForDistribution(config) { - return withDangerousMod(config, [ - 'ios', - async (cfg) => { - const iosDir = path.join(cfg.modRequest.projectRoot, 'ios'); - patchPodfile(iosDir); - return cfg; - }, - ]); -} - -function patchPodfile(iosDir) { - const podfilePath = path.join(iosDir, 'Podfile'); - const podfile = fs.readFileSync(podfilePath, 'utf8'); - - // Insert our hook *before* the existing post_install block so that React - // Native's own post_install (react_native_post_install, etc.) still runs - // afterwards. mergeContents uses the tagged marker comments to make the - // injection idempotent across repeated `expo prebuild` runs. - const result = mergeContents({ - tag: 'with-sentry-build-for-distribution', - src: podfile, - newSrc: HOOK, - anchor: /^(\s*)post_install do \|installer\|/m, - offset: 0, - comment: '#', - }); - - if (result.didMerge) { - fs.writeFileSync(podfilePath, result.contents); - } -} - -module.exports = withSentryBuildForDistribution; From 92618bf319fadd7354d16fa0d0e47407c6a225eb Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 16:15:12 +0100 Subject: [PATCH 42/45] fix: remove Oppo device from e2e tests The Oppo Reno 6-11.0 is no longer supported by Browserstack --- .github/workflows/e2e-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3e6680b..3168bf3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -227,7 +227,6 @@ jobs: "Huawei Nova 11 SE-12.0", "Motorola Moto G71 5G-11.0", "OnePlus 13R-15.0", - "Oppo Reno 6-11.0", "Samsung Galaxy Note 9-8.1", "Vivo Y21-11.0", "Xiaomi Redmi Note 11-11.0" From 401f68233112d048275a663af243fb79c06a91a1 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 17:14:07 +0100 Subject: [PATCH 43/45] fix(build): don't drop overridden packages when collecting native modules npm list --parseable --long appends flag fields (:OVERRIDDEN, :INVALID, ...) after name@version, so taking the last :-field read "OVERRIDDEN" for the override-pinned sodium-native@5.1.0. The module was silently dropped from prebuild download + jniLibs packaging while the bundled JS still referenced it, crashing every device with ERR_DLOPEN_FAILED: libsodium-native__5.1.0.so not found. Co-Authored-By: Claude Fable 5 --- scripts/lib/native-modules.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/lib/native-modules.ts b/scripts/lib/native-modules.ts index 09d6489..fec7545 100644 --- a/scripts/lib/native-modules.ts +++ b/scripts/lib/native-modules.ts @@ -47,7 +47,12 @@ export async function collectNativePairs( })`npm list --all --parseable --long --production`; for (const line of npmListResult.stdout) { - const moduleInfo = line.split(":").at(-1); + // `--parseable --long` lines are `:@` plus + // optional trailing flag fields (e.g. `:OVERRIDDEN` for packages + // resolved via package.json `overrides`, `:INVALID`, ...). Take + // the field after the path — `.at(-1)` reads a flag when present, + // which silently dropped sodium-native from the shipped addons. + const moduleInfo = line.split(":")[1]; if (!moduleInfo) continue; deps.add(moduleInfo); } From f52ed59d476c0dc9586ac3078483b8b9159c0a38 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 17:14:08 +0100 Subject: [PATCH 44/45] fix(ios): build Sentry pods with library evolution for Xcode 26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Xcode 26's Swift compiler hides SentrySDK.startTransaction (used by SentryNativeBridge) when sentry-cocoa builds from source without BUILD_LIBRARY_FOR_DISTRIBUTION (getsentry/sentry-cocoa#7950). Set it on Sentry pod targets from the library config plugin so every consumer's prebuild gets the workaround, injected inside the existing post_install block (CocoaPods allows only one post_install hook — the previous attempt added a second block, which fails pod install). Register the plugin in the e2e app, which didn't apply it before. Verified locally with Xcode 26.3: the exact CI archive command now succeeds, and the Release app boots on the iOS simulator. Co-Authored-By: Claude Fable 5 --- app.plugin.js | 34 ++++ apps/e2e/app.json | 5 +- apps/e2e/package-lock.json | 378 +++++++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+), 1 deletion(-) diff --git a/app.plugin.js b/app.plugin.js index dbd5203..21353b9 100644 --- a/app.plugin.js +++ b/app.plugin.js @@ -27,7 +27,11 @@ import configPlugins from "@expo/config-plugins"; import { createRequire } from "node:module"; const { withAndroidManifest } = configPlugins; const { withInfoPlist } = configPlugins; +const { withPodfile } = configPlugins; const require = createRequire(import.meta.url); +const { + mergeContents, +} = require("@expo/config-plugins/build/utils/generateCode"); // Manifest meta-data on the main `` tag is shared // across processes within the package. @@ -78,9 +82,39 @@ function withComapeoCore(config, props) { const moduleIdent = sentry ? readModuleIdentification() : null; config = withSentryAndroid(config, sentry, moduleIdent); config = withSentryIos(config, sentry); + config = withSentryLibraryEvolution(config); return config; } +// getsentry/sentry-cocoa#7950: Xcode 26's Swift compiler drops +// `SentrySDK.startTransaction` (and other Swift-only APIs) from the +// Sentry module unless the pod builds with library evolution. +// `SentryNativeBridge.swift` calls that API, so every consumer needs +// this. Inserted INSIDE the existing `post_install` block because +// CocoaPods allows only one `post_install` hook per Podfile. +const SENTRY_LIBRARY_EVOLUTION_HOOK = `\ + installer.pods_project.targets.each do |target| + if target.name.start_with?('Sentry') + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + end + end + end`; + +function withSentryLibraryEvolution(config) { + return withPodfile(config, (cfg) => { + cfg.modResults.contents = mergeContents({ + tag: "comapeo-core-sentry-library-evolution", + src: cfg.modResults.contents, + newSrc: SENTRY_LIBRARY_EVOLUTION_HOOK, + anchor: /post_install do \|installer\|/, + offset: 1, + comment: "#", + }).contents; + return cfg; + }); +} + /** * Module version label + bundled-backend dep map — the same values * `src/version.ts` exposes to the RN-side `initSentry`. Used only on diff --git a/apps/e2e/app.json b/apps/e2e/app.json index fdd77ad..da60a31 100644 --- a/apps/e2e/app.json +++ b/apps/e2e/app.json @@ -22,6 +22,9 @@ }, "package": "com.comapeo.core.e2e" }, - "plugins": [["expo-dev-client", { "launchMode": "most-recent" }]] + "plugins": [ + ["expo-dev-client", { "launchMode": "most-recent" }], + "../../app.plugin.js" + ] } } diff --git a/apps/e2e/package-lock.json b/apps/e2e/package-lock.json index 22791ea..780f859 100644 --- a/apps/e2e/package-lock.json +++ b/apps/e2e/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { + "@sentry/react-native": "^7.13.0", "expo": "55.0.19", "expo-crypto": "55.0.14", "react": "19.2.5", @@ -4195,6 +4196,335 @@ "integrity": "sha512-bTM24b5v4qN3h52oflnv+OujFORn/kVi06WaWhnQQw14/ycilPqIsqsa+DpIBqdBrXxvLa9fXtCRrQtGATZCEw==", "license": "MIT" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.38.0.tgz", + "integrity": "sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.38.0.tgz", + "integrity": "sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.38.0.tgz", + "integrity": "sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.38.0", + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.38.0.tgz", + "integrity": "sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.38.0", + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.9.1.tgz", + "integrity": "sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.38.0.tgz", + "integrity": "sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.38.0", + "@sentry-internal/feedback": "10.38.0", + "@sentry-internal/replay": "10.38.0", + "@sentry-internal/replay-canvas": "10.38.0", + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/cli": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz", + "integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==", + "hasInstallScript": true, + "license": "FSL-1.1-MIT", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.58.4", + "@sentry/cli-linux-arm": "2.58.4", + "@sentry/cli-linux-arm64": "2.58.4", + "@sentry/cli-linux-i686": "2.58.4", + "@sentry/cli-linux-x64": "2.58.4", + "@sentry/cli-win32-arm64": "2.58.4", + "@sentry/cli-win32-i686": "2.58.4", + "@sentry/cli-win32-x64": "2.58.4" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz", + "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==", + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz", + "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==", + "cpu": [ + "arm" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz", + "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz", + "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz", + "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz", + "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz", + "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz", + "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sentry/core": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.38.0.tgz", + "integrity": "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.38.0.tgz", + "integrity": "sha512-3UiKo6QsqTyPGUt0XWRY9KLaxc/cs6Kz4vlldBSOXEL6qPDL/EfpwNJT61osRo81VFWu8pKu7ZY2bvLPryrnBQ==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.38.0", + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/react-native": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-7.13.0.tgz", + "integrity": "sha512-pKY+rln3CEnhnG21eUXcl/yeG5fkNruqXXjEikE5FjsyLmJi2POAGwT4tlupyHtG3fQT5mp06QI7bQdpAuH4Xw==", + "license": "MIT", + "dependencies": { + "@sentry/babel-plugin-component-annotate": "4.9.1", + "@sentry/browser": "10.38.0", + "@sentry/cli": "2.58.4", + "@sentry/core": "10.38.0", + "@sentry/react": "10.38.0", + "@sentry/types": "10.38.0" + }, + "bin": { + "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" + }, + "peerDependencies": { + "expo": ">=49.0.0", + "react": ">=17.0.0", + "react-native": ">=0.65.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@sentry/types": { + "version": "10.38.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-10.38.0.tgz", + "integrity": "sha512-DoeyTv/TvnoVDhHgdyv/wehieAKdyjLjEMtPOqqq/AjkP02BxeC0JYUrrWKOjV0wdLq5ZP8jKcCX8GN7awZonQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.38.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -10203,6 +10533,26 @@ "node": ">=10" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", @@ -11198,6 +11548,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -13465,6 +13821,12 @@ "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", "license": "MIT" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -13751,12 +14113,28 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/whatwg-url-minimum": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", From 7758fda66b64d2782e699d34ac60999cbdc7a3d8 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Wed, 10 Jun 2026 21:38:15 +0100 Subject: [PATCH 45/45] fix(ci): cache android/libnode in the iOS build job too The backend build reads the Node ABI from android/libnode headers on every platform. The iOS job only created them on a cache miss (the download script fetches both platforms); a cache hit restored just ios/NodeMobile.xcframework, so any rerun failed with ENOENT on node_version.h. Cache both paths and bump the key so ios-only caches can't hit. Co-Authored-By: Claude Fable 5 --- .github/workflows/e2e-tests.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3168bf3..f49335f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -114,11 +114,19 @@ jobs: id: cache-ios-framework uses: actions/cache@v4 with: - path: ios/NodeMobile.xcframework + # android/libnode is needed even on iOS builds: + # `readNodeJsMobileVersions()` in scripts/lib/node-versions.ts + # reads the Node ABI from the android libnode headers. The + # download script fetches both platforms, but a cache hit used + # to restore only the xcframework, breaking rerun builds. + path: | + ios/NodeMobile.xcframework + android/libnode # hashFiles(...) folds the BASE_URL (defined inside the # download script) into the cache key, so any swap of the - # source repo automatically invalidates stale caches. - key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-ios-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} + # source repo automatically invalidates stale caches. The -v2 + # suffix invalidates older caches that lack android/libnode. + key: nodejs-mobile-${{ env.NODEJS_MOBILE_VERSION }}-ios-v2-${{ hashFiles('scripts/download-nodejs-mobile.sh') }} - name: Download nodejs-mobile binaries if: steps.cache-ios-framework.outputs.cache-hit != 'true'