diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03d1868..c5e98f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,17 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Android emulator configuration + ANDROID_API_LEVEL: '35' + ANDROID_ARCH: 'x86_64' + ANDROID_PROFILE: 'pixel_6' + ANDROID_AVD_NAME: 'Pixel_8_API_35' + # iOS simulator configuration + IOS_DEVICE_MODEL: 'iPhone 17 Pro' + IOS_VERSION: '26.0' + XCODE_VERSION: '26.1.1' + jobs: lint: runs-on: ubuntu-latest @@ -44,7 +55,7 @@ jobs: - name: Build package run: yarn prepare - build-android: + harness-android: runs-on: ubuntu-latest env: @@ -65,28 +76,43 @@ jobs: restore-keys: | ${{ runner.os }}-turborepo-android- - - name: Check turborepo cache for Android + - name: Check turborepo cache status run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") + # Check if harness tests can be skipped entirely + HARNESS_CACHE_STATUS=$(node -p "($(yarn turbo run test:harness:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'test:harness:android').cache.status") + if [[ $HARNESS_CACHE_STATUS == "HIT" ]]; then + echo "harness_cache_hit=1" >> $GITHUB_ENV + fi - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV + # Check if native build can be skipped (to avoid installing JDK/Gradle) + BUILD_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") + if [[ $BUILD_CACHE_STATUS == "HIT" ]]; then + echo "build_cache_hit=1" >> $GITHUB_ENV fi + - name: Reclaim disk space + if: env.harness_cache_hit != 1 + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true + - name: Install JDK - if: env.turbo_cache_hit != 1 + if: env.harness_cache_hit != 1 && env.build_cache_hit != 1 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: 'zulu' java-version: '17' - name: Finalize Android SDK - if: env.turbo_cache_hit != 1 + if: env.harness_cache_hit != 1 && env.build_cache_hit != 1 run: | /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" - name: Cache Gradle - if: env.turbo_cache_hit != 1 + if: env.harness_cache_hit != 1 && env.build_cache_hit != 1 uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | @@ -97,16 +123,64 @@ jobs: ${{ runner.os }}-gradle- - name: Build example for Android + if: env.harness_cache_hit != 1 env: JAVA_OPTS: "-XX:MaxHeapSize=6g" run: | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - build-ios: + - name: Enable KVM group perms + if: env.harness_cache_hit != 1 + 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 + ls /dev/kvm + + - name: AVD cache + if: env.harness_cache_hit != 1 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ env.ANDROID_API_LEVEL }}-${{ env.ANDROID_ARCH }} + + - name: Create AVD and generate snapshot for caching + if: env.harness_cache_hit != 1 && steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.ANDROID_API_LEVEL }} + arch: ${{ env.ANDROID_ARCH }} + profile: ${{ env.ANDROID_PROFILE }} + disk-size: 1G + heap-size: 1G + force-avd-creation: false + avd-name: ${{ env.ANDROID_AVD_NAME }} + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: echo "Generated AVD snapshot for caching." + + - name: Run Harness E2E tests + if: env.harness_cache_hit != 1 + uses: reactivecircus/android-emulator-runner@v2 + with: + working-directory: example + api-level: ${{ env.ANDROID_API_LEVEL }} + arch: ${{ env.ANDROID_ARCH }} + force-avd-creation: false + avd-name: ${{ env.ANDROID_AVD_NAME }} + disable-animations: true + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: | + adb install -r "./android/app/build/outputs/apk/debug/app-debug.apk" + yarn turbo run test:harness:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" + + harness-ios: runs-on: macos-latest env: - XCODE_VERSION: 16.3 TURBO_CACHE_DIR: .turbo/ios RCT_USE_RN_DEP: 1 RCT_USE_PREBUILT_RNCORE: 1 @@ -126,22 +200,28 @@ jobs: restore-keys: | ${{ runner.os }}-turborepo-ios- - - name: Check turborepo cache for iOS + - name: Check turborepo cache status run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + # Check if harness tests can be skipped entirely + HARNESS_CACHE_STATUS=$(node -p "($(yarn turbo run test:harness:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'test:harness:ios').cache.status") + if [[ $HARNESS_CACHE_STATUS == "HIT" ]]; then + echo "harness_cache_hit=1" >> $GITHUB_ENV + fi - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV + # Check if native build can be skipped (to avoid installing Xcode/CocoaPods) + BUILD_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + if [[ $BUILD_CACHE_STATUS == "HIT" ]]; then + echo "build_cache_hit=1" >> $GITHUB_ENV fi - name: Use appropriate Xcode version - if: env.turbo_cache_hit != 1 + if: env.harness_cache_hit != 1 && env.build_cache_hit != 1 uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: xcode-version: ${{ env.XCODE_VERSION }} - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + if: env.harness_cache_hit != 1 && env.build_cache_hit != 1 run: | cd example bundle install @@ -149,5 +229,26 @@ jobs: bundle exec pod install --project-directory=ios - name: Build example for iOS + if: env.harness_cache_hit != 1 run: | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" + + - name: Setup iOS Simulator + if: env.harness_cache_hit != 1 + uses: futureware-tech/simulator-action@v4 + with: + model: ${{ env.IOS_DEVICE_MODEL }} + os: iOS + os_version: ${{ env.IOS_VERSION }} + wait_for_boot: true + erase_before_boot: false + + - name: Install app on simulator + if: env.harness_cache_hit != 1 + run: | + xcrun simctl install booted example/ios/build/Build/Products/Debug-iphonesimulator/WebworkerExample.app + + - name: Run Harness E2E tests + if: env.harness_cache_hit != 1 + run: | + yarn turbo run test:harness:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" diff --git a/example/package.json b/example/package.json index ea8a870..210a7a5 100644 --- a/example/package.json +++ b/example/package.json @@ -6,8 +6,8 @@ "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", - "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", - "build:ios": "react-native build-ios --mode Debug", + "build:android": "react-native build-android --tasks assembleDebug --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=x86_64\"", + "build:ios": "react-native build-ios --mode Debug --buildFolder build", "test:harness:ios": "react-native-harness --harnessRunner ios", "test:harness:android": "react-native-harness --harnessRunner android" }, diff --git a/example/rn-harness.config.mjs b/example/rn-harness.config.mjs index 27988d4..3ffa9f9 100644 --- a/example/rn-harness.config.mjs +++ b/example/rn-harness.config.mjs @@ -14,7 +14,7 @@ const config = { runners: [ androidPlatform({ name: 'android', - device: androidEmulator('Pixel_8_API_33', { + device: androidEmulator('Pixel_8_API_35', { apiLevel: 35, profile: 'pixel_6', diskSize: '1G', @@ -24,7 +24,7 @@ const config = { }), applePlatform({ name: 'ios', - device: appleSimulator('iPhone 16 Pro', '26.0'), + device: appleSimulator('iPhone 17 Pro', '26.0'), bundleId: 'webworker.example', }), ], diff --git a/turbo.json b/turbo.json index 8b2bf08..35ba962 100644 --- a/turbo.json +++ b/turbo.json @@ -7,17 +7,16 @@ "env": ["ANDROID_HOME", "ORG_GRADLE_PROJECT_newArchEnabled"], "inputs": [ "package.json", + "cpp", "android", "!android/build", - "src/*.ts", - "src/*.tsx", "example/package.json", "example/android", "!example/android/.gradle", "!example/android/build", "!example/android/app/build" ], - "outputs": [] + "outputs": ["example/android/app/build/outputs/apk/debug/app-debug.apk"] }, "build:ios": { "env": [ @@ -28,11 +27,47 @@ ], "inputs": [ "package.json", + "cpp", + "*.podspec", + "ios", + "example/package.json", + "example/ios", + "!example/ios/build", + "!example/ios/Pods" + ], + "outputs": [ + "example/ios/build/Build/Products/Debug-iphonesimulator/WebworkerExample.app/**" + ] + }, + "test:harness:android": { + "dependsOn": ["build:android"], + "inputs": [ + "package.json", + "cpp", + "android", + "!android/build", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/src", + "example/android", + "!example/android/.gradle", + "!example/android/build", + "!example/android/app/build" + ], + "outputs": [] + }, + "test:harness:ios": { + "dependsOn": ["build:ios"], + "inputs": [ + "package.json", + "cpp", "*.podspec", "ios", "src/*.ts", "src/*.tsx", "example/package.json", + "example/src", "example/ios", "!example/ios/build", "!example/ios/Pods"