From 18c33844088cf21c776ea55a09fa5ddda6739450 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Wed, 14 Oct 2026 14:05:57 +0800 Subject: [PATCH 1/2] ci: build the Expo example on iOS and Android Nothing compiled the Expo integration: the podspec's Expo detection and ExpoModulesCore version gate, the Swift ReactDelegateHandler and the Kotlin module only build inside a `use_expo_modules!` app, and the e2e app is bare React Native. SDK 57 already broke that path once and it surfaced as a user report. expo-build builds Example/expoUsePushy against the local package (the same copy e2e-common links): iOS Release for the simulator after asserting the Expo subspec, EXPO_SUPPORTS_BUNDLEURL and the autolinked handler were all selected, and Android Release after `expo prebuild`. It runs only when the Expo integration's inputs change and joins the native-required gate when it runs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/expo_build.yml | 148 ++++++++++++++++++++++++++++++ .github/workflows/native-gate.yml | 7 +- 2 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/expo_build.yml diff --git a/.github/workflows/expo_build.yml b/.github/workflows/expo_build.yml new file mode 100644 index 00000000..44377720 --- /dev/null +++ b/.github/workflows/expo_build.yml @@ -0,0 +1,148 @@ +name: expo-build + +# Builds the Expo example (Example/expoUsePushy, `use_expo_modules!`) against +# the local package on both platforms. Nothing else compiles the Expo side: +# the podspec's Expo detection (Podfile scan, expo/expo-modules-core lookup, +# the ExpoModulesCore version gate that selects EXPO_SUPPORTS_BUNDLEURL), the +# Swift ReactDelegateHandler and the Kotlin module. SDK 57 removed the +# createBridge fallback and broke exactly this path once; the e2e app is a +# bare React Native project and never reaches it. +# +# Build only (no simulator/emulator run): the update flow itself is covered +# by e2e-ios / e2e-android. Paths are limited to the Expo integration's +# inputs; changes to the native .mm/.java sources are already compiled by +# the e2e workflows. +on: + pull_request: + branches: + - '**' + paths: &expo-paths + - '.github/workflows/expo_build.yml' + - '.github/actions/e2e-common/**' + - '.gitmodules' + - 'package.json' + - 'react-native-update.podspec' + - 'react-native.config.js' + - 'expo-module.config.json' + - 'src/Native*.ts' + - 'ios/Expo/**' + - 'ios/RCTPushy/*.h' + - 'ios/PrivacyInfo.xcprivacy' + - 'android/build.gradle' + - 'android/src/main/java/expo/**' + - 'Example/expoUsePushy/**' + push: + branches: + - master + paths: *expo-paths + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Same toolchain as e2e-ios. + XCODE_VERSION: '26.4' + EXPO_DIR: Example/expoUsePushy + +jobs: + expo-ios: + name: expo-build (ios) + runs-on: macos-26 + timeout-minutes: 45 + steps: + - name: Checkout react-native-update + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Set up Expo example + # Node/Bun, the example's dependencies and the local + # react-native-update link (the same copy an npm install ships). + uses: ./.github/actions/e2e-common + with: + project-dir: ${{ env.EXPO_DIR }} + checkout-cli: 'false' + bun-cache-scope: -expo + bun-cache-key-suffix: -${{ hashFiles('Example/expoUsePushy/bun.lock') }} + + - name: Cache CocoaPods downloads + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/Library/Caches/CocoaPods + key: ${{ runner.os }}-cocoapods-expo-${{ hashFiles('Example/expoUsePushy/ios/Podfile.lock', 'Example/expoUsePushy/ios/Podfile', 'Example/expoUsePushy/bun.lock') }} + restore-keys: | + ${{ runner.os }}-cocoapods-expo- + + - name: Install CocoaPods dependencies + env: + LANG: en_US.UTF-8 + run: cd "$EXPO_DIR/ios" && pod install + + - name: Assert the Expo integration was selected + # A silent fallback to the bare-RN pod (detection failed) would still + # build; fail instead, as users on that path would get no Expo hook. + run: | + set -euo pipefail + grep -F 'react-native-update/Expo' "$EXPO_DIR/ios/Podfile.lock" + grep -F 'EXPO_SUPPORTS_BUNDLEURL' \ + "$EXPO_DIR/ios/Pods/Target Support Files/react-native-update/react-native-update.release.xcconfig" + # Expo autolinking found expo-module.config.json and registered the + # bundle URL handler. + grep -F 'ExpoPushyReactDelegateHandler' \ + "$EXPO_DIR/ios/Pods/Target Support Files/Pods-expoUsePushy/ExpoModulesProvider.swift" + + - name: Build (Release, simulator) + # arm64 only (the runner's own architecture): a generic simulator + # destination otherwise compiles every pod twice, for x86_64 too. + run: | + set -o pipefail + cd "$EXPO_DIR/ios" + xcodebuild -workspace expoUsePushy.xcworkspace -scheme expoUsePushy \ + -configuration Release -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build CODE_SIGNING_ALLOWED=NO ARCHS=arm64 build \ + | tee xcodebuild.log | grep -E '(error|warning: .*RCTPushy|BUILD (SUCCEEDED|FAILED))' || true + grep -q 'BUILD SUCCEEDED' xcodebuild.log + + expo-android: + name: expo-build (android) + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - name: Checkout react-native-update + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + + - name: Setup Java + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: '17' + cache: gradle + cache-dependency-path: | + Example/expoUsePushy/bun.lock + Example/expoUsePushy/app.json + + - name: Set up Expo example + uses: ./.github/actions/e2e-common + with: + project-dir: ${{ env.EXPO_DIR }} + checkout-cli: 'false' + bun-cache-scope: -expo + bun-cache-key-suffix: -${{ hashFiles('Example/expoUsePushy/bun.lock') }} + + - name: Generate the Android project + # The example keeps no android/ directory; prebuild generates it the + # way an Expo user's CNG build does. + run: cd "$EXPO_DIR" && CI=1 bunx expo prebuild --platform android --no-install + + - name: Build (Release) + # One ABI is enough to compile and link every module. + run: cd "$EXPO_DIR/android" && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a diff --git a/.github/workflows/native-gate.yml b/.github/workflows/native-gate.yml index e08bc7bf..6ababb65 100644 --- a/.github/workflows/native-gate.yml +++ b/.github/workflows/native-gate.yml @@ -7,10 +7,11 @@ name: native-gate # it (runs are created at event time, before any of them completes) and does # not block; a run that exists must succeed. e2e-harmony is # workflow_dispatch-only (no hosted HarmonyOS runner) and is therefore not -# part of the gate; harmony-build (HAR compile in the pinned container) is. +# part of the gate; harmony-build (HAR compile in the pinned container) and +# expo-build (the Expo example on both platforms) are. on: workflow_run: - workflows: [lint, test, e2e-android, e2e-ios, harmony-build] + workflows: [lint, test, e2e-android, e2e-ios, harmony-build, expo-build] types: [completed] permissions: @@ -28,7 +29,7 @@ jobs: const sha = context.payload.workflow_run.head_sha; const { owner, repo } = context.repo; const alwaysRequired = ['lint', 'test']; - const requiredWhenPresent = ['e2e-android', 'e2e-ios', 'harmony-build']; + const requiredWhenPresent = ['e2e-android', 'e2e-ios', 'harmony-build', 'expo-build']; const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { owner, repo, head_sha: sha, per_page: 100, }); From 24f2f8163b37f159ebf73706ef9b20a8473c8c59 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Wed, 14 Oct 2026 14:18:06 +0800 Subject: [PATCH 2/2] ci(expo-build): read-only token, no persisted checkout credentials Co-Authored-By: Claude Opus 5.5 --- .github/workflows/expo_build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/expo_build.yml b/.github/workflows/expo_build.yml index 44377720..70662bfc 100644 --- a/.github/workflows/expo_build.yml +++ b/.github/workflows/expo_build.yml @@ -36,6 +36,10 @@ on: - master paths: *expo-paths +# Builds pull request code: the token only needs to read the repository. +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -55,6 +59,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive + persist-credentials: false - name: Setup Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 @@ -119,6 +124,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive + persist-credentials: false - name: Setup Java uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1