Bundle, build and run iOS test app in separate steps in production + ccache enabled #1209
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check | |
| env: | |
| CMAKE_VERSION: 4.1.2 | |
| # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts | |
| NDK_VERSION: 27.1.12297006 | |
| # Enabling the Gradle test on CI (disabled by default because it downloads a lot) | |
| ENABLE_GRADLE_TESTS: true | |
| # pnpm records GitHub git dependencies (e.g. node-addon-examples) with an SSH | |
| # repo URL, which anonymous CI runners can't clone. Rewrite git@github.com: to | |
| # HTTPS so the public repo is fetched over HTTPS. Provided as ad-hoc git config | |
| # via env so it applies to every job without a per-job step. | |
| GIT_CONFIG_COUNT: 1 | |
| GIT_CONFIG_KEY_0: "url.https://github.com/.insteadOf" | |
| GIT_CONFIG_VALUE_0: "git@github.com:" | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Every job sets Node.js up before pnpm, which is the opposite of what pnpm's | |
| # own docs suggest. pnpm/action-setup v6 bootstraps pnpm by shelling out to | |
| # `npm ci`, and it finds npm on PATH — on our self-hosted Linux runner the only | |
| # npm there is the broken copy in the runner's own Node.js 24 externals, so the | |
| # setup step dies with "Cannot find module '../lib/cli.js'". Running setup-node | |
| # first puts a working npm ahead of it. The usual ordering exists so that | |
| # setup-node's `cache: pnpm` can shell out to `pnpm store path`; pnpm's own | |
| # `cache: true` caches the same store without needing pnpm to already exist. | |
| # | |
| # hendrikmuhs/ccache-action is pinned to an exact patch, not a floating major: | |
| # its v1 / v1.2 tags still point at a Node.js 20 build, which the runners now | |
| # warn about. Float again once upstream moves them to >= v1.2.22. | |
| jobs: | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| # Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting | |
| # TODO: Remove this once we have a way to run linting without building the native code | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: tools platform-tools ndk;${{ env.NDK_VERSION }} | |
| - run: rustup target add x86_64-linux-android | |
| - run: pnpm install | |
| - run: pnpm run build | |
| # Bootstrap weak-node-api and ferric-example to get types | |
| # TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command | |
| - run: pnpm --filter weak-node-api run bootstrap | |
| - run: pnpm --filter @react-native-node-api/ferric-example run bootstrap | |
| - run: pnpm run lint | |
| env: | |
| DEBUG: eslint:eslint | |
| - run: pnpm run prettier:check | |
| - run: pnpm run depcheck | |
| - run: pnpm run publint | |
| unit-tests: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runner: | |
| - ubuntu-latest | |
| - windows-latest | |
| - macos-latest | |
| runs-on: ${{ matrix.runner }} | |
| name: Unit tests (${{ matrix.runner }}) | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: tools platform-tools ndk;${{ env.NDK_VERSION }} | |
| - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim x86_64-apple-ios | |
| - run: pnpm install | |
| - run: pnpm run bootstrap | |
| - run: pnpm test | |
| weak-node-api-tests: | |
| if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'weak-node-api') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| runner: | |
| - ubuntu-latest | |
| - windows-latest | |
| - macos-latest | |
| runs-on: ${{ matrix.runner }} | |
| name: Weak Node-API tests (${{ matrix.runner }}) | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| - run: pnpm install | |
| - run: pnpm run build | |
| - name: Prepare weak-node-api | |
| run: pnpm --filter weak-node-api run prebuild:prepare | |
| - name: Build and run weak-node-api C++ tests | |
| run: | | |
| cmake -S . -B build -DBUILD_TESTS=ON | |
| cmake --build build | |
| ctest --test-dir build --output-on-failure | |
| working-directory: packages/weak-node-api | |
| test-ios: | |
| if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple 🍎') | |
| name: Test app (iOS) | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| # Device names change with every Xcode release, so pick one dynamically. | |
| - name: Boot the simulator | |
| run: | | |
| SIMULATOR_UDID=$(xcrun simctl list devices available --json | node -e " | |
| const { devices } = JSON.parse(require('node:fs').readFileSync(0, 'utf8')); | |
| const runtimes = Object.keys(devices).filter((r) => r.includes('iOS')).sort(); | |
| const device = runtimes.flatMap((r) => devices[r]).find((d) => d.name.startsWith('iPhone')); | |
| if (!device) throw new Error('Found no available iPhone simulator'); | |
| console.log(device.udid); | |
| ") | |
| echo "Booting $SIMULATOR_UDID" | |
| xcrun simctl bootstatus "$SIMULATOR_UDID" -b | |
| echo "SIMULATOR_UDID=$SIMULATOR_UDID" >> $GITHUB_ENV | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| # The action defaults to 500M, which a from-source React Native build | |
| # fills mid-build: it evicted 88 times in one run, discarding most of | |
| # what it had just cached. | |
| max-size: 3G | |
| - run: rustup target add aarch64-apple-ios-sim x86_64-apple-ios | |
| - run: pnpm install | |
| - run: pnpm run bootstrap | |
| env: | |
| CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim | |
| FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios | |
| # Mirrors React Native's scripts/xcode/ccache.conf, which only applied | |
| # while ccache ran through their wrapper script (it sets CCACHE_CONFIGPATH | |
| # to it) and so no longer does now that it runs as a compiler launcher. | |
| # Without this sloppiness Xcode's modules, PCH and index-store flags leave | |
| # ccache treating almost every compile as uncacheable. | |
| - name: Tune ccache for Xcode | |
| run: | | |
| ccache --set-config sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros | |
| ccache --set-config file_clone=true | |
| ccache --set-config depend_mode=true | |
| ccache --set-config inode_cache=true | |
| # Must precede `pod install`: react-native-test-app embeds the resources | |
| # declared in app.json when generating the workspace, skipping missing | |
| # ones, and the app would then expect a Metro dev server at runtime. | |
| # `--assets-dest dist` lands assets in dist/assets, as app.json expects. | |
| - name: Bundle test app | |
| run: pnpm exec react-native bundle --entry-file index.ts --platform ios --dev false --minify false --bundle-output dist/main.ios.jsbundle --assets-dest dist | |
| working-directory: apps/test-app | |
| # No USE_CCACHE: it points CC/CXX at React Native's ccache-clang.sh, whose | |
| # bare `clang` resolves to setup-cpp's LLVM instead of Xcode's, breaking | |
| # every system module. Ccache goes on the xcodebuild invocation instead. | |
| # | |
| # Retries the intermittent CocoaPods/CocoaPods#12866 "pathname contains | |
| # null byte" crash, and only that. Drop once CocoaPods > 1.17.0 fixes it. | |
| - name: Install pods | |
| shell: bash | |
| working-directory: apps/test-app | |
| run: | | |
| for attempt in 1 2 3; do | |
| if pnpm run pod-install 2>&1 | tee "$RUNNER_TEMP/pod-install.log"; then | |
| exit 0 | |
| fi | |
| if ! grep -q 'pathname contains null byte' "$RUNNER_TEMP/pod-install.log"; then | |
| echo "::error::pod install failed for an unrelated reason" | |
| exit 1 | |
| fi | |
| echo "::warning::pod install hit CocoaPods/CocoaPods#12866 (attempt ${attempt}/3), retrying" | |
| done | |
| echo "::error::pod install kept failing with CocoaPods/CocoaPods#12866" | |
| exit 1 | |
| # Raw xcodebuild output, not piped through xcbeautify — see #391. | |
| # C_COMPILER_LAUNCHER keeps Xcode's own clang and just prefixes it with | |
| # ccache; explicit modules needs opting in when a launcher is used. | |
| - name: Build test app | |
| run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination "generic/platform=iOS Simulator" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES C_COMPILER_LAUNCHER=ccache CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES | |
| working-directory: apps/test-app/ios | |
| - name: Run test app | |
| run: | | |
| # Install the app | |
| xcrun simctl install "$SIMULATOR_UDID" ./ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app | |
| # Run Mocha Remote wrapping the launch of the app | |
| pnpm exec mocha-remote --exit-on-error -- xcrun simctl launch --terminate-running-process --console-pty "$SIMULATOR_UDID" com.microsoft.ReactTestApp | |
| working-directory: apps/test-app | |
| env: | |
| MOCHA_REMOTE_CONTEXT: allTests | |
| test-macos: | |
| # Disabling this on main for now, as initializing the template takes a long time and | |
| # we don't have macOS-specific code yet. Currently also blocked on react-native-macos: | |
| # https://github.com/callstackincubator/react-native-node-api/issues/392 | |
| if: contains(github.event.pull_request.labels.*.name, 'MacOS 💻') | |
| name: Test app (macOS) | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - run: rustup target add x86_64-apple-darwin | |
| - run: pnpm install | |
| - run: pnpm run bootstrap | |
| env: | |
| CMAKE_RN_TRIPLETS: arm64;x86_64-apple-darwin | |
| FERRIC_TARGETS: aarch64-apple-darwin,x86_64-apple-darwin | |
| - run: pnpm run init-macos-test-app | |
| - run: pod install --project-directory=macos | |
| working-directory: apps/macos-test-app | |
| - name: Bundle test app | |
| run: npx react-native bundle --entry-file index.js --platform macos --dev false --minify false --bundle-output dist/main.macos.jsbundle --assets-dest dist/res | |
| working-directory: apps/macos-test-app | |
| - name: Build test app | |
| run: xcodebuild archive -workspace MacOSTestApp.xcworkspace -configuration Release -scheme MacOSTestApp-macOS -destination generic/platform="macOS" -archivePath ./build/macos-test-app.xcarchive | |
| working-directory: apps/macos-test-app/macos | |
| - name: Run test app | |
| run: npx mocha-remote --exit-on-error -- macos/build/macos-test-app.xcarchive/Products/Applications/MacOSTestApp.app/Contents/MacOS/MacOSTestApp | |
| working-directory: apps/macos-test-app | |
| env: | |
| MOCHA_REMOTE_CONTEXT: allTests | |
| test-android: | |
| # Temporarily gated to labeled PRs only: the ubuntu-self-hosted runner is | |
| # offline, so running this on main pushes leaves the job queued forever and | |
| # blocks the whole Check run from completing. Re-enable on main once the | |
| # self-hosted runner is back. Tracked in #379. | |
| if: contains(github.event.pull_request.labels.*.name, 'Android 🤖') | |
| name: Test app (Android) | |
| runs-on: ubuntu-self-hosted | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: tools platform-tools ndk;${{ env.NDK_VERSION }} cmake;${{ env.CMAKE_VERSION }} | |
| - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim | |
| - run: pnpm install | |
| - run: pnpm run bootstrap | |
| env: | |
| CMAKE_RN_TRIPLETS: x86_64-linux-android | |
| FERRIC_TARGETS: x86_64-linux-android | |
| - name: Clone patched Hermes version | |
| shell: bash | |
| run: | | |
| REACT_NATIVE_OVERRIDE_HERMES_DIR=$(pnpm exec react-native-node-api vendor-hermes --silent) | |
| echo "REACT_NATIVE_OVERRIDE_HERMES_DIR=$REACT_NATIVE_OVERRIDE_HERMES_DIR" >> $GITHUB_ENV | |
| working-directory: apps/test-app | |
| # - name: Setup Android Emulator cache | |
| # uses: actions/cache@v6 | |
| # id: avd-cache | |
| # with: | |
| # path: | | |
| # ~/.android/avd/* | |
| # ~/.android/adb* | |
| # key: ${{ runner.os }}-avd-29 | |
| # See https://github.com/marketplace/actions/android-emulator-runner#running-hardware-accelerated-emulators-on-linux-runners | |
| - name: Enable KVM group perms | |
| 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 | |
| - name: Build weak-node-api for all Android architectures | |
| run: pnpm --filter weak-node-api run prebuild:build:android | |
| - name: Build ferric-example for all architectures | |
| run: pnpm run build --android | |
| working-directory: packages/ferric-example | |
| - name: Run tests (Android) | |
| timeout-minutes: 75 | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 29 | |
| force-avd-creation: false | |
| disable-animations: true | |
| arch: x86_64 | |
| ndk: ${{ env.NDK_VERSION }} | |
| cmake: 3.22.1 | |
| working-directory: apps/test-app | |
| script: | | |
| # Setup port forwarding to Mocha Remote | |
| adb reverse tcp:8090 tcp:8090 | |
| # Uninstall the app if already in the snapshot (unlikely but could result in a signature mismatch failure) | |
| adb uninstall com.microsoft.reacttestapp || true | |
| # Start logcat in background and save logs | |
| adb logcat > emulator-logcat.txt 2>&1 & | |
| LOGCAT_PID=$! | |
| # Build, install and run the app | |
| pnpm run test:android:allTests --mode Release | |
| # Wait a bit for the sub-process to terminate, before terminating the emulator | |
| sleep 5 | |
| # Stop logcat | |
| kill $LOGCAT_PID || true | |
| - name: Upload device logs | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: emulator-logcat | |
| path: apps/test-app/emulator-logcat.txt | |
| test-ferric-apple-triplets: | |
| if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Ferric 🦀') | |
| name: Test ferric Apple triplets | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: lts/krypton | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - name: Setup cpp tools | |
| uses: aminya/setup-cpp@v1 | |
| with: | |
| clang-format: true | |
| - name: ccache | |
| uses: hendrikmuhs/ccache-action@v1.2.23 | |
| with: | |
| key: ${{ github.job }}-${{ runner.os }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - run: rustup target add x86_64-apple-darwin x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim | |
| - run: rustup toolchain install nightly --component rust-src | |
| - run: pnpm install | |
| - run: pnpm run build | |
| - name: Build weak-node-api for all Apple architectures | |
| run: | | |
| pnpm --filter weak-node-api run prebuild:prepare | |
| pnpm --filter weak-node-api run prebuild:build:apple | |
| # Build Ferric example for all Apple architectures | |
| - run: pnpm exec ferric --apple | |
| working-directory: packages/ferric-example | |
| - name: Inspect the structure of the prebuilt binary | |
| run: lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-info.txt | |
| working-directory: packages/ferric-example | |
| - name: Upload lipo info | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: lipo-info | |
| path: packages/ferric-example/lipo-info.txt | |
| - name: Verify Apple triplet builds | |
| run: | | |
| # Create expected fixture content | |
| cat > expected-lipo-info.txt << 'EOF' | |
| Architectures in the fat file: ferric_example.apple.node/ios-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 | |
| Architectures in the fat file: ferric_example.apple.node/macos-arm64_x86_64/libferric_example.framework/libferric_example are: x86_64 arm64 | |
| Architectures in the fat file: ferric_example.apple.node/tvos-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 | |
| Non-fat file: ferric_example.apple.node/ios-arm64/libferric_example.framework/libferric_example is architecture: arm64 | |
| Non-fat file: ferric_example.apple.node/tvos-arm64/libferric_example.framework/libferric_example is architecture: arm64 | |
| Non-fat file: ferric_example.apple.node/xros-arm64-simulator/libferric_example.framework/libferric_example is architecture: arm64 | |
| Non-fat file: ferric_example.apple.node/xros-arm64/libferric_example.framework/libferric_example is architecture: arm64 | |
| EOF | |
| # Compare with expected fixture (will fail if files differ) | |
| diff expected-lipo-info.txt lipo-info.txt | |
| working-directory: packages/ferric-example |