diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 2d017cfd..4cb7d1a9 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -21,8 +21,34 @@ jobs: - name: Check public API compatibility run: npm run check:api-compat - android-test: + android-unit-test: needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: jdx/mise-action@v4 + + - name: Cache npm packages + uses: actions/cache@v6 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json', 'test/test.ts') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm install + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run Android unit tests + working-directory: android + run: ./gradlew :app:test + + android-test: + needs: [lint, android-unit-test] runs-on: bitrise-react-native-code-push-linux-runner strategy: matrix: @@ -49,6 +75,16 @@ jobs: - name: Install dependencies run: npm install + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + with: + # This job's Gradle build lives in a test app generated at runtime by the test harness, + # not a project checked into this repo, so there's nothing meaningful for it to + # contribute back to the cache. Read-only avoids each matrix variant (bare/expo) writing + # its own redundant entry; it still reuses whatever android-unit-test wrote, since + # setup-gradle shares its cache across jobs in the same workflow run. + cache-read-only: true + - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules diff --git a/CLAUDE.md b/CLAUDE.md index 1e3d54df..02cdf64b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,13 @@ React Native CodePush is a native module that enables over-the-air updates for R ## Development Commands ### Testing -- `npm test` - Run all tests with TypeScript compilation + +#### Unit tests + +- `cd android && ./gradlew :app:test` +- iOS: no unit tests yet. + +#### E2E Tests - `npm run test:android` - Run Android-specific tests - `npm run test:ios` - Run iOS-specific tests - `npm run test:setup-android` - Set up Android emulator for testing @@ -34,7 +40,7 @@ React Native CodePush is a native module that enables over-the-air updates for R ### Platform Structure - **iOS**: `ios/` - Objective-C implementation with CocoaPods integration -- **Android**: `android/` - Java implementation with Gradle plugin +- **Android**: `android/` - Java/Kotlin implementation with Gradle plugin - **Windows**: `windows/` - C++ implementation for Windows React Native - **JavaScript**: Root level - TypeScript definitions and bridge code @@ -48,7 +54,7 @@ React Native CodePush is a native module that enables over-the-air updates for R - **Custom Test Runner**: TypeScript-based test framework in `test/` - **Real App Testing**: Creates actual React Native apps for integration testing - **Scenario Testing**: Update, rollback, and error scenarios -- **No unit test infra yet**: this repo only has the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first. +- **No unit test infra for JS/iOS yet**: JS/iOS only have the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first. - **Templates**: `test/template/` holds native files (Podfile, AppDelegate, Android app files) and JS scenarios copied over top of a freshly generated RN/Expo app during test setup, overwriting its defaults — edit files here, not the generated project, for changes to persist - **`test:ios` vs `test:setup:ios` vs `test:fast:ios`**: `test:ios` is just `test:setup:ios` followed by `test:fast:ios` — the two are meant to be split apart for local iteration. - `test:setup:ios` (mocha `--ios --setup`) boots the simulator and provisions the test app once: copies templates, runs `pod install`, patches Info.plist/AppDelegate. It never builds or runs any test scenario. diff --git a/Examples/CodePushDemo/android/gradle.properties b/Examples/CodePushDemo/android/gradle.properties index 9afe6159..8dd6ab52 100644 --- a/Examples/CodePushDemo/android/gradle.properties +++ b/Examples/CodePushDemo/android/gradle.properties @@ -42,3 +42,9 @@ hermesEnabled=true # This allows your app to draw behind system bars for an immersive UI. # Note: Only works with ReactActivity and should not be used with custom Activity. edgeToEdgeEnabled=false + +# Opt out of AGP 9's built-in Kotlin support and new DSL, matching what RN's own 0.87 app template +# does. See the AGP v9 adoption RFC: +# https://github.com/react-native-community/discussions-and-proposals/pull/1006). +android.builtInKotlin=false +android.newDsl=false diff --git a/android/app/build.gradle b/android/app/build.gradle index 9e5eeac9..95938db6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,4 +1,21 @@ -apply plugin: "com.android.library" +// No versions specified: both plugins are expected to already be resolved on the root project's +// buildscript classpath, which every RN app template declares (AGP for the app itself, Kotlin +// because RN ships Kotlin internally). +// Matches how other RN libraries (e.g. reanimated) apply these plugins. +plugins { + id "com.android.library" + id "org.jetbrains.kotlin.android" apply false +} + +// AGP 9 provides Kotlin support built in; applying the classic kotlin-android plugin on top of it +// leads to a configuration-time failure. Below AGP 9, and on AGP 9+ when `android.builtInKotlin=false` +// opts back out of built-in Kotlin, the classic plugin is still required. +// See React Native RFC about ecosystem migration details: https://github.com/react-native-community/discussions-and-proposals/pull/1006 +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger() +def builtInKotlinEnabled = agpMajor >= 9 && (!project.hasProperty("android.builtInKotlin") || Boolean.parseBoolean(project.property("android.builtInKotlin").toString())) +if (!builtInKotlinEnabled) { + apply plugin: "org.jetbrains.kotlin.android" +} def isNewArchitectureEnabled() { // To opt-in for the New Architecture, you can either: @@ -10,15 +27,18 @@ def isNewArchitectureEnabled() { def IS_NEW_ARCHITECTURE_ENABLED = isNewArchitectureEnabled() -def DEFAULT_COMPILE_SDK_VERSION = 26 -def DEFAULT_BUILD_TOOLS_VERSION = "26.0.3" -def DEFAULT_TARGET_SDK_VERSION = 26 -def DEFAULT_MIN_SDK_VERSION = 16 +// These are fallbacks only. Keep them aligned with RN's current app template +// so a consumer relying on the fallback still gets a build that actually works +// with a current RN version. +def DEFAULT_COMPILE_SDK_VERSION = 35 +def DEFAULT_BUILD_TOOLS_VERSION = "36.0.0" +def DEFAULT_TARGET_SDK_VERSION = 35 +def DEFAULT_MIN_SDK_VERSION = 24 android { namespace "com.microsoft.codepush.react" - compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION + compileSdk rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION defaultConfig { @@ -40,9 +60,25 @@ android { buildFeatures { buildConfig true } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +// Only set when we applied the classic kotlin-android plugin ourselves above: AGP 9's built-in +// Kotlin integration doesn't expose this extension at all, per Android's own built-in +// Kotlin migration guide: https://developer.android.com/build/migrate-to-built-in-kotlin +if (!builtInKotlinEnabled) { + android.kotlinOptions { + jvmTarget = "17" + } } dependencies { implementation 'com.facebook.react:react-android:0.82.1' implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' + + testImplementation 'junit:junit:4.13.2' } diff --git a/android/app/src/test/java/com/microsoft/codepush/react/FileUtilsTest.kt b/android/app/src/test/java/com/microsoft/codepush/react/FileUtilsTest.kt new file mode 100644 index 00000000..971c68e8 --- /dev/null +++ b/android/app/src/test/java/com/microsoft/codepush/react/FileUtilsTest.kt @@ -0,0 +1,31 @@ +package com.microsoft.codepush.react + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class FileUtilsTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + @Test + fun copyDirectoryContents_copiesNestedFilesAndSubdirectories() { + val sourceDir = tempFolder.newFolder("source") + File(sourceDir, "root.txt").writeText("root contents") + val nestedDir = File(sourceDir, "nested").apply { mkdir() } + File(nestedDir, "child.txt").writeText("child contents") + + val destinationDir = File(tempFolder.root, "destination") + + FileUtils.copyDirectoryContents(sourceDir.absolutePath, destinationDir.absolutePath) + + assertEquals("root contents", File(destinationDir, "root.txt").readText()) + val copiedNestedFile = File(destinationDir, "nested/child.txt") + assertTrue(copiedNestedFile.exists()) + assertEquals("child contents", copiedNestedFile.readText()) + } +} diff --git a/android/build.gradle b/android/build.gradle index 989373d7..a45423e2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,12 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.12.0") + // Must match (or exceed) the AGP version RN's own @react-native/gradle-plugin declares: that plugin is + // included as a composite build below and its own AGP dependency wins Gradle's classpath + // conflict resolution regardless of what's pinned here, so an out-of-date pin here is + // silently overridden rather than actually enforced. Bump this in lockstep with RN's own + // gradle-plugin version whenever bumping the react-native devDependency. + classpath("com.android.tools.build:gradle:9.2.1") classpath("com.facebook.react:react-native-gradle-plugin") // NOTE: Do not place your application dependencies here; they belong diff --git a/android/gradle.properties b/android/gradle.properties index 89e0d99e..d4d82b9d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -16,3 +16,10 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +android.useAndroidX=true + +# https://github.com/react-native-community/discussions-and-proposals/pull/1006 +# Applies to this repo's own standalone build/test harness only; see android/app/build.gradle's comments +# for why downstream consumers may still land in either mode. +android.builtInKotlin=false +android.newDsl=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 74b269f3..da80f8d1 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip