Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -67,7 +103,18 @@ jobs:
target: google_apis
arch: x86
disable-animations: true
script: npm run ${{ matrix.test-command }}
# connectedAndroidTest is included here (rather than a separate job or step) to reuse
# this job's already-booted emulator, even though it means it runs once per matrix
# variant. The android-emulator-runner action has no post-cleanup step, so a second
# step would boot and tear down a second emulator; ::group:: markers keep the two
# test runs visually separated in the Actions log instead.
script: |
echo "::group::Instrumented tests"
(cd android && ./gradlew :app:connectedAndroidTest)
echo "::endgroup::"
echo "::group::E2E tests"
npm run ${{ matrix.test-command }}
echo "::endgroup::"

ios-test:
needs: lint
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ Examples/testapp_rn
# Android debug build files (conflict ignoring #Visual Studio files)
!android/app/src/debug/

# CMake/NDK build cache for android/app/src/main/cpp
android/app/.cxx

# iOS Simulator crash reports swept up from failed test runs (see test/test.ts)
test/crash-logs/

Expand Down
5 changes: 4 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ test/
# Remove after this framework is published on NPM
code-push-plugin-testing-framework/

# Android build artifacts and Android Studio bits
# Android build artifacts, test sources and Android Studio bits
android/app/build
android/app/.cxx
android/app/src/test
android/app/src/androidTest
android/local.properties
android/.gradle
android/**/*.iml
Expand Down
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions Examples/CodePushDemo/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
67 changes: 61 additions & 6 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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 {
Expand All @@ -27,6 +47,21 @@ android {
versionCode 1
versionName "1.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", IS_NEW_ARCHITECTURE_ENABLED.toString()

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

externalNativeBuild {
cmake {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}

lintOptions {
Expand All @@ -40,9 +75,29 @@ 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'

androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
}
2 changes: 2 additions & 0 deletions android/app/src/androidTest/assets/bad_header/old.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Irrelevant content - old.dat just needs to open successfully so the
test reaches the diff-header check this fixture is actually exercising.
Binary file not shown.
25 changes: 25 additions & 0 deletions android/app/src/androidTest/assets/basic/new.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function greet(name) {
console.log("Hello there, " + name + "!");
return "Hello there, " + name + "!";
}

function farewell(name) {
console.log("Goodbye, " + name + ".");
return "Goodbye, " + name + ".";
}

function shout(name) {
console.log("HEY, " + name.toUpperCase() + "!!!");
return "HEY, " + name.toUpperCase() + "!!!";
}

var VERSION = "1.1.0";
var BUILD_NUMBER = 43;

module.exports = {
greet: greet,
farewell: farewell,
shout: shout,
VERSION: VERSION,
BUILD_NUMBER: BUILD_NUMBER,
};
19 changes: 19 additions & 0 deletions android/app/src/androidTest/assets/basic/old.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function greet(name) {
console.log("Hello, " + name + "!");
return "Hello, " + name + "!";
}

function farewell(name) {
console.log("Goodbye, " + name + ".");
return "Goodbye, " + name + ".";
}

var VERSION = "1.0.0";
var BUILD_NUMBER = 42;

module.exports = {
greet: greet,
farewell: farewell,
VERSION: VERSION,
BUILD_NUMBER: BUILD_NUMBER,
};
Binary file not shown.
3 changes: 3 additions & 0 deletions android/app/src/androidTest/assets/empty_old/new.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Everything in this file is new: the old side is a zero-byte file, so
the whole patch body is a literal insert with no copy-from-old control
entries at all.
Empty file.
Binary file not shown.
4 changes: 4 additions & 0 deletions android/app/src/androidTest/assets/identical/new.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This file is byte-for-byte identical on both sides of the patch.
It exercises the zero-delta path: a real BSDIFF40 patch whose only
control entry is a single full-length copy from the old file, with
no literal bytes and no seek. Nothing to add or skip.
4 changes: 4 additions & 0 deletions android/app/src/androidTest/assets/identical/old.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This file is byte-for-byte identical on both sides of the patch.
It exercises the zero-delta path: a real BSDIFF40 patch whose only
control entry is a single full-length copy from the old file, with
no literal bytes and no seek. Nothing to add or skip.
Binary file not shown.
3 changes: 3 additions & 0 deletions android/app/src/androidTest/assets/wrong_old/old.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This is a completely unrelated old file, deliberately shaped so that
applying fixtures/basic/patch.bsdiff against it does not match the old
file bsdiff was actually built from.
Loading