From f3ec2c00e84600c3859e47026f16a387ce190b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Kapu=C5=9Bciak?= Date: Wed, 11 Nov 2026 11:21:21 +0100 Subject: [PATCH 1/2] ci(e2e): run the web, iOS, and Android e2e suites on GitHub Actions The iOS job builds a Release app, creates an iPhone 17 Pro simulator on the pinned iOS 26.5 runtime, and passes its UDID to the ios target through E2E_IOS_DEVICE. --- .github/workflows/e2e.yml | 138 ++++++++++++++++++++++++++++++++++++++ e2e.config.ts | 3 +- 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..dc17deb --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,138 @@ +name: e2e + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + web: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: npm + - run: npm ci + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package-lock.json') }} + + - name: Install browsers + run: npx e2e-web install chromium --with-deps + + # The web target starts Metro itself (app.command in e2e.config.ts). + - name: Run e2e + run: npx e2e run --target web --reporter list,junit + env: + AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} + + - name: Upload report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-web + path: .e2e + if-no-files-found: warn + retention-days: 7 + + ios: + runs-on: macos-26 + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: npm + - run: npm ci + + # ios/ is generated (CNG); expo run:ios prebuilds it, installs pods, and builds without a simulator. + - name: Build for the simulator + run: npx expo run:ios --configuration Release --device generic --no-bundler --output build/ios + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.agent-device/apple-runner/derived + key: agent-device-runner-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - name: Boot a simulator and install the app + run: | + UDID=$(xcrun simctl create e2e "iPhone 17 Pro" com.apple.CoreSimulator.SimRuntime.iOS-26-5) + xcrun simctl bootstatus "$UDID" -b + APP=$(find build/ios -maxdepth 3 -name 'e2ereactnative.app' -type d | head -n 1) + echo "Installing $APP" + xcrun simctl install "$UDID" "$APP" + echo "E2E_IOS_DEVICE=$UDID" >> "$GITHUB_ENV" + + - name: Run e2e + run: npx e2e run --target ios --workers 1 --reporter list,junit + env: + AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: e2e-ios + path: .e2e + if-no-files-found: warn + retention-days: 7 + + android: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: npm + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + distribution: temurin + java-version: 17 + - run: npm ci + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + # android/ is generated (CNG). The Expo template signs release builds with the debug keystore. + - name: Build the APK + run: | + npx expo prebuild --platform android --no-install + cd android && ./gradlew :app:assembleRelease + + - name: Run on an emulator + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + with: + api-level: 36 + arch: x86_64 + target: google_apis + profile: pixel_7 + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim + disable-animations: true + script: | + adb install -r android/app/build/outputs/apk/release/app-release.apk + npx e2e run --target android --workers 1 --reporter list,junit + env: + AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: e2e-android + path: .e2e + if-no-files-found: warn + retention-days: 7 diff --git a/e2e.config.ts b/e2e.config.ts index 57e69e6..1ac472a 100644 --- a/e2e.config.ts +++ b/e2e.config.ts @@ -24,7 +24,8 @@ export default { targets: [ { name: 'ios', - engine: mobile({ platform: 'ios', device: 'iPhone 17 Pro' }), + // CI passes the UDID of a simulator it created on a pinned iOS runtime. + engine: mobile({ platform: 'ios', device: process.env.E2E_IOS_DEVICE || 'iPhone 17 Pro' }), // A development build made with `npx expo run:ios`; Metro serves the JS bundle. app: { bundleId: 'dev.e2e.podcasts', From ab6ff6dca1ceea95b06dd29e1cf285396a3818cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Kapu=C5=9Bciak?= Date: Wed, 11 Nov 2026 11:30:59 +0100 Subject: [PATCH 2/2] ci(e2e): skip the e2e jobs on pull requests from forks Forks get no repository secrets, so the agent steps would fail without AI_GATEWAY_API_KEY. --- .github/workflows/e2e.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dc17deb..8ffd005 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,6 +14,8 @@ concurrency: jobs: web: + # Fork pull requests get no secrets, so the agent steps can't call the model. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -47,6 +49,7 @@ jobs: retention-days: 7 ios: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: macos-26 timeout-minutes: 60 steps: @@ -88,6 +91,7 @@ jobs: retention-days: 7 android: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest timeout-minutes: 60 steps: