From 487b06e305b920500625ee28b55f0f1c643561d1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 1 Oct 2026 12:50:48 +0200 Subject: [PATCH 1/3] ci: consolidate workflows and reduce repeated native builds --- .github/ci-paths.json | 189 ++++++++++++++++ .github/dependabot.yml | 31 ++- .github/workflows/build-android.yml | 96 --------- .github/workflows/build-ios.yml | 123 ----------- .github/workflows/build-macos.yml | 120 ----------- .github/workflows/ci.yml | 189 ++++++++++++++++ .github/workflows/docs.yml | 52 ----- .github/workflows/lint-cpp.yml | 32 --- .github/workflows/lint-typescript.yml | 99 --------- .github/workflows/release.yml | 9 + .github/workflows/test-cpp.yml | 127 ----------- .github/workflows/test-harness-android.yml | 105 +++++---- .github/workflows/test-harness-ios.yml | 107 ++++----- .github/workflows/test-macos.yml | 98 +++++---- .github/workflows/update-lockfiles.yml | 95 ++++---- example/ios/Podfile | 11 +- example/ios/Podfile.lock | 6 +- scripts/ci-dependencies.test.ts | 157 ++++++++++++++ scripts/ci-dependencies.ts | 157 ++++++++++++++ scripts/ci-pod-lockfiles.rb | 33 +++ scripts/ci-pod-lockfiles.test.rb | 43 ++++ scripts/ci.test.ts | 240 +++++++++++++++++++++ scripts/ci.ts | 82 +++++++ scripts/test-cpp.sh | 83 +++++++ 24 files changed, 1437 insertions(+), 847 deletions(-) create mode 100644 .github/ci-paths.json delete mode 100644 .github/workflows/build-android.yml delete mode 100644 .github/workflows/build-ios.yml delete mode 100644 .github/workflows/build-macos.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/lint-cpp.yml delete mode 100644 .github/workflows/lint-typescript.yml delete mode 100644 .github/workflows/test-cpp.yml create mode 100644 scripts/ci-dependencies.test.ts create mode 100644 scripts/ci-dependencies.ts create mode 100644 scripts/ci-pod-lockfiles.rb create mode 100644 scripts/ci-pod-lockfiles.test.rb create mode 100644 scripts/ci.test.ts create mode 100644 scripts/ci.ts create mode 100644 scripts/test-cpp.sh diff --git a/.github/ci-paths.json b/.github/ci-paths.json new file mode 100644 index 00000000..1a96d696 --- /dev/null +++ b/.github/ci-paths.json @@ -0,0 +1,189 @@ +{ + "typescript": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + ".prettierrc.js", + "config/**", + "tsconfig.json", + "package.json", + "bun.lock", + "patches/**", + "example/**/*.ts", + "example/**/*.tsx", + "example/**/*.js", + "example/**/*.jsx", + "example/**/*.mjs", + "example/tsconfig.json", + "example/app.json", + "example/package.json", + "example/macos/package.json", + "packages/react-native-nitro-sqlite/**/*.ts", + "packages/react-native-nitro-sqlite/**/*.tsx", + "packages/react-native-nitro-sqlite/**/*.js", + "packages/react-native-nitro-sqlite/**/*.jsx", + "packages/react-native-nitro-sqlite/tsconfig.json", + "packages/react-native-nitro-sqlite/tsconfig.build.json", + "packages/react-native-nitro-sqlite/package.json", + "packages/react-native-nitro-sqlite/nitro.json", + "packages/react-native-nitro-sqlite-vec/**/*.ts", + "packages/react-native-nitro-sqlite-vec/**/*.tsx", + "packages/react-native-nitro-sqlite-vec/**/*.js", + "packages/react-native-nitro-sqlite-vec/**/*.jsx", + "packages/react-native-nitro-sqlite-vec/tsconfig.json", + "packages/react-native-nitro-sqlite-vec/package.json", + "scripts/**/*.ts", + "example/jest*.js" + ], + "cpp_format": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + ".clang-format", + "scripts/clang-format.sh", + "packages/react-native-nitro-sqlite/cpp/**", + "packages/react-native-nitro-sqlite/tests/cpp/**" + ], + "cpp_tests": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + "packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.*", + "packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.*", + "packages/react-native-nitro-sqlite/cpp/NitroSQLiteExecuteBatch.*", + "packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.*", + "packages/react-native-nitro-sqlite/cpp/NitroSQLiteStatementGroup.hpp", + "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*", + "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3-symbol-prefix.h", + "packages/react-native-nitro-sqlite-vec/cpp/**", + "packages/react-native-nitro-sqlite/tests/cpp/**", + "scripts/generate-sqlite-symbol-prefix.py", + "scripts/test-private-sqlite-symbols.sh", + "scripts/test-cpp.sh" + ], + "docs": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + "docs/**", + "packages/react-native-nitro-sqlite/src/**", + "packages/react-native-nitro-sqlite/package.json", + "packages/react-native-nitro-sqlite-vec/src/**", + "packages/react-native-nitro-sqlite-vec/package.json", + "config/tsconfig.json", + "package.json", + "bun.lock" + ], + "android": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + "example/android/**", + "example/src/tests/**", + "example/tests/**", + "example/rn-harness.config.mjs", + "example/jest.config.js", + "packages/react-native-nitro-sqlite/cpp/**", + "packages/react-native-nitro-sqlite/android/**", + "packages/react-native-nitro-sqlite/src/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/android/**", + "packages/react-native-nitro-sqlite-vec/cpp/**", + "bun.lock", + "package.json", + "example/package.json", + "packages/react-native-nitro-sqlite/package.json", + "packages/react-native-nitro-sqlite-vec/package.json", + "patches/react-native-nitro-modules*", + "example/react-native.config.js", + "packages/react-native-nitro-sqlite/react-native.config.js", + "packages/react-native-nitro-sqlite-vec/react-native.config.js", + "packages/react-native-nitro-sqlite/nitro.json", + "!packages/react-native-nitro-sqlite/src/__tests__/**", + "scripts/android-sqlite-flags/**", + "example/index.js", + "example/babel.config*.js", + "example/app.json", + "packages/react-native-nitro-sqlite-vec/src/**", + "!packages/react-native-nitro-sqlite-vec/src/__tests__/**" + ], + "ios": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + "example/ios/**", + "example/src/tests/**", + "example/tests/**", + "example/rn-harness.config.mjs", + "example/jest.config.js", + "packages/react-native-nitro-sqlite/cpp/**", + "packages/react-native-nitro-sqlite/ios/**", + "packages/react-native-nitro-sqlite/src/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**", + "packages/react-native-nitro-sqlite-vec/cpp/**", + "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec", + "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec", + "example/Gemfile.lock", + "bun.lock", + "package.json", + "example/package.json", + "packages/react-native-nitro-sqlite/package.json", + "packages/react-native-nitro-sqlite-vec/package.json", + "patches/react-native-nitro-modules*", + "example/react-native.config.js", + "packages/react-native-nitro-sqlite/react-native.config.js", + "packages/react-native-nitro-sqlite-vec/react-native.config.js", + "packages/react-native-nitro-sqlite/nitro.json", + "!packages/react-native-nitro-sqlite/src/__tests__/**", + "scripts/test-podspec-threadsafe.rb", + "example/index.js", + "example/babel.config*.js", + "example/app.json", + "packages/react-native-nitro-sqlite-vec/src/**", + "!packages/react-native-nitro-sqlite-vec/src/__tests__/**" + ], + "macos": [ + ".github/workflows/**", + ".github/actions/**", + ".github/ci-paths.json", + "scripts/ci*", + "example/macos/**", + "example/src/**", + "example/tests/**", + "example/app.json", + "example/index.js", + "example/babel.config*.js", + "packages/react-native-nitro-sqlite/src/**", + "packages/react-native-nitro-sqlite/cpp/**", + "packages/react-native-nitro-sqlite/ios/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**", + "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**", + "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec", + "packages/react-native-nitro-sqlite/react-native.config.js", + "packages/react-native-nitro-sqlite/nitro.json", + "packages/react-native-nitro-sqlite/package.json", + "packages/react-native-nitro-sqlite-vec/src/**", + "packages/react-native-nitro-sqlite-vec/cpp/**", + "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec", + "packages/react-native-nitro-sqlite-vec/react-native.config.js", + "packages/react-native-nitro-sqlite-vec/package.json", + "patches/**", + "example/macos/Podfile.lock", + "example/Gemfile.lock", + "bun.lock", + "package.json", + "example/package.json", + "example/macos/package.json", + "example/react-native.config.js", + "example/macos/react-native.config.js", + "!packages/react-native-nitro-sqlite/src/__tests__/**", + "!packages/react-native-nitro-sqlite-vec/src/__tests__/**" + ] +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d3fbb3e0..299145bc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,20 +4,31 @@ updates: - package-ecosystem: 'github-actions' directory: '/' schedule: - interval: 'daily' + interval: 'weekly' + day: 'monday' labels: - 'dependencies' - 'ecosystem:github-actions' + groups: + actions: + patterns: ['*'] + update-types: ['minor', 'patch'] - package-ecosystem: 'gradle' directories: - '/packages/react-native-nitro-sqlite/android/' - '/example/android/' schedule: - interval: 'daily' + interval: 'weekly' + day: 'monday' labels: - 'dependencies' - 'ecosystem:gradle' + groups: + gradle: + patterns: ['*'] + update-types: ['minor', 'patch'] + group-by: 'dependency-name' # Update the example wrapper with its React Native Android toolchain. ignore: - dependency-name: 'gradle-wrapper' @@ -28,7 +39,8 @@ updates: - '/packages/react-native-nitro-sqlite/' - '/example/' schedule: - interval: 'daily' + interval: 'weekly' + day: 'monday' labels: - 'dependencies' - 'ecosystem:npm' @@ -62,3 +74,16 @@ updates: babel: patterns: - '@babel/*' + update-types: ['minor', 'patch'] + development: + patterns: + - '@types/*' + - '*jest*' + - '*eslint*' + - 'typescript*' + - 'prettier' + update-types: ['minor', 'patch'] + shared-dependencies: + patterns: ['*'] + update-types: ['minor', 'patch'] + group-by: 'dependency-name' diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml deleted file mode 100644 index 86baf432..00000000 --- a/.github/workflows/build-android.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Build Android - -on: - push: - branches: - - main - paths: &android_paths - - ".github/workflows/build-android.yml" - - "example/android/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/android/**" - - "packages/react-native-nitro-sqlite/src/specs/**" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/android/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "scripts/android-sqlite-flags/**" - - "bun.lock" - - "package.json" - - "example/package.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/react-native-nitro-modules*" - - "example/react-native.config.js" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - pull_request: - paths: *android_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -env: - # Build a single ABI in CI to reduce native compile time. - TARGET_ANDROID_ARCH: x86_64 - -jobs: - build: - name: Build Android Example App - runs-on: ubuntu-24.04 - strategy: - matrix: - min-sdk: - - "24" - - "26" - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - - - name: Install npm dependencies (bun) - run: bun install - - - name: Set minSdk in build.gradle - run: sed -i "s/minSdkVersion = 24/minSdkVersion = ${{ matrix.min-sdk }}/g" example/android/build.gradle - - - name: Setup JDK 17 - uses: actions/setup-java@v6 - with: - distribution: "zulu" - java-version: 17 - java-package: jdk - - - name: Restore Gradle/CMake cache - uses: actions/cache@v6 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - example/android/.gradle - example/android/app/.cxx - key: ${{ runner.os }}-gradle-${{ matrix.min-sdk }}-${{ env.TARGET_ANDROID_ARCH }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/CMakeLists.txt', '**/*.cmake') }} - restore-keys: | - ${{ runner.os }}-gradle-${{ matrix.min-sdk }}-${{ env.TARGET_ANDROID_ARCH }}- - ${{ runner.os }}-gradle- - - name: Test Android SQLite build flags - run: example/android/gradlew --project-dir scripts/android-sqlite-flags testSqliteFlags --no-daemon - - - name: Check Gradle configuration cache - if: matrix.min-sdk == '24' - working-directory: example/android - run: | - # React Native creates the autolinking file on the first run. - ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon - ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon - ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon | tee "$RUNNER_TEMP/nitro-sqlite-configuration-cache.log" - grep -q 'Configuration cache entry reused' "$RUNNER_TEMP/nitro-sqlite-configuration-cache.log" - - - name: Run Gradle Build for example/android/ - working-directory: example/android - run: ./gradlew :app:assembleDebug --no-daemon --build-cache -PreactNativeArchitectures=${{ env.TARGET_ANDROID_ARCH }} - - # Gradle cache doesn't like daemons - - name: Stop Gradle Daemon - working-directory: example/android - run: ./gradlew --stop diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml deleted file mode 100644 index 522c5b4c..00000000 --- a/.github/workflows/build-ios.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Build iOS - -on: - push: - branches: - - main - paths: &ios_paths - - ".github/workflows/build-ios.yml" - - "example/ios/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**" - - "packages/react-native-nitro-sqlite/src/specs/**" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/ios/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec" - - "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec" - - "scripts/test-podspec-threadsafe.rb" - - "example/Gemfile.lock" - - "bun.lock" - - "package.json" - - "example/package.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/react-native-nitro-modules*" - - "example/react-native.config.js" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - pull_request: - paths: *ios_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -env: - USE_CCACHE: 1 - -jobs: - build: - name: Build iOS Example App - runs-on: macos-26 - strategy: - matrix: - include: - - name: static_frameworks - use_frameworks: static - - name: no_frameworks - use_frameworks: "" # intentionally unset - steps: - - uses: actions/checkout@v7 - - - name: Test SQLite thread-safety pod configuration - run: ruby scripts/test-podspec-threadsafe.rb - - - uses: oven-sh/setup-bun@v2 - - - name: Set USE_FRAMEWORKS - if: matrix.use_frameworks != '' - run: echo "USE_FRAMEWORKS=${{ matrix.use_frameworks }}" >> $GITHUB_ENV - - - name: Install npm dependencies (bun) - run: bun install - - - name: Install Ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - max-size: 1.5G - key: ${{ runner.os }}-${{ matrix.use_frameworks }}-ccache-example-ios - create-symlink: true - - name: Setup ccache behavior - run: | - echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV - echo "CCACHE_FILECLONE=true" >> $GITHUB_ENV - echo "CCACHE_DEPEND=true" >> $GITHUB_ENV - echo "CCACHE_INODECACHE=true" >> $GITHUB_ENV - - - name: Setup Ruby (bundle) - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.3.0 - bundler-cache: true - working-directory: example - - - name: Select Xcode 26.5 - run: sudo xcode-select -s "/Applications/Xcode_26.5.app/Contents/Developer" - - - name: Restore Pods cache - uses: actions/cache@v6 - with: - path: example/ios/Pods - key: ${{ runner.os }}-${{ matrix.use_frameworks }}-pods-v2-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock') }} - - name: Clean generated iOS codegen - run: rm -rf example/ios/build/generated - - name: Install Pods - working-directory: example - run: bun pods - - - name: Restore DerivedData cache - uses: actions/cache@v6 - with: - path: example/ios/build/DerivedData - key: ${{ runner.os }}-${{ matrix.use_frameworks }}-dd-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock', '**/package.json', '**/bun.lock') }}-xcode26.5 - restore-keys: | - ${{ runner.os }}-${{ matrix.use_frameworks }}-dd-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock', '**/package.json', '**/bun.lock') }}-xcode26.5 - - - name: Build App - working-directory: example/ios - run: | - set -o pipefail - xcodebuild \ - CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ - -derivedDataPath build/DerivedData -UseModernBuildSystem=YES \ - -workspace NitroSQLiteExample.xcworkspace \ - -scheme NitroSQLiteExample \ - -sdk iphonesimulator \ - -configuration Debug \ - -destination 'generic/platform=iOS Simulator' \ - -showBuildTimingSummary \ - ONLY_ACTIVE_ARCH=YES \ - build \ - CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml deleted file mode 100644 index a252a4a3..00000000 --- a/.github/workflows/build-macos.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Build macOS - -on: - push: - branches: - - main - paths: &macos_paths - - ".github/workflows/build-macos.yml" - - "example/macos/**" - - "example/src/**" - - "example/app.json" - - "example/index.js" - - "example/babel.config*.js" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/ios/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**" - - "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/src/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/react-native-nitro-modules*" - - "example/macos/Podfile.lock" - - "example/Gemfile.lock" - - "bun.lock" - - "package.json" - - "example/package.json" - - "example/macos/package.json" - - "example/react-native.config.js" - - "example/macos/react-native.config.js" - - "!example/macos/scripts/test-macos*.mjs" - - "!example/src/tests/**" - - "!packages/react-native-nitro-sqlite/src/__tests__/**" - - "!packages/react-native-nitro-sqlite-vec/src/__tests__/**" - pull_request: - paths: *macos_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -env: - USE_CCACHE: 1 - -jobs: - build: - name: Build macOS Example App - runs-on: macos-26 - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - - - name: Install npm dependencies (bun) - run: bun install - - - name: Install Ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - max-size: 1.5G - key: ${{ runner.os }}-ccache-example-macos - create-symlink: true - - name: Setup ccache behavior - run: | - { - echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" - echo "CCACHE_FILECLONE=true" - echo "CCACHE_DEPEND=true" - echo "CCACHE_INODECACHE=true" - } >> "$GITHUB_ENV" - - - name: Setup Ruby (bundle) - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.3.0 - bundler-cache: true - working-directory: example - - - name: Select Xcode 26.5 - run: sudo xcode-select -s "/Applications/Xcode_26.5.app/Contents/Developer" - - - name: Restore Pods cache - uses: actions/cache@v6 - with: - path: example/macos/Pods - key: ${{ runner.os }}-pods-macos-${{ hashFiles('example/macos/Podfile.lock', 'example/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods-macos- - - name: Clean generated macOS codegen - run: rm -rf example/macos/build/generated - - name: Install Pods - run: bun --cwd example pods:macos - - - name: Restore DerivedData cache - uses: actions/cache@v6 - with: - path: example/macos/build/DerivedData - key: ${{ runner.os }}-dd-macos-${{ hashFiles('example/macos/Podfile.lock', 'example/Gemfile.lock', '**/package.json', '**/bun.lock') }}-xcode26.5 - restore-keys: | - ${{ runner.os }}-dd-macos-${{ hashFiles('example/macos/Podfile.lock', 'example/Gemfile.lock', '**/package.json', '**/bun.lock') }}-xcode26.5 - - - name: Build App - working-directory: example/macos - run: | - set -o pipefail - xcodebuild \ - -derivedDataPath build/DerivedData -UseModernBuildSystem=YES \ - -workspace NitroSQLiteExample.xcworkspace \ - -scheme NitroSQLiteExample-macOS \ - -configuration Debug \ - -destination 'platform=macOS,arch=arm64' \ - -showBuildTimingSummary \ - ONLY_ACTIVE_ARCH=YES \ - build \ - CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6459e165 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,189 @@ +name: CI + +on: + push: + branches: [main] + paths: &ci_paths + - '.github/**' + - 'packages/**' + - 'example/**' + - 'docs/**' + - 'scripts/**' + - 'config/**' + - 'patches/**' + - 'package.json' + - 'bun.lock' + - 'tsconfig.json' + - '.clang-format' + - '.prettierrc.js' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: *ci_paths + schedule: + - cron: '17 5 * * 1' + workflow_dispatch: + inputs: + full: + description: 'Run all checks and secondary native configurations' + type: boolean + default: true + workflow_call: + inputs: + full: + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.full || github.event_name == 'schedule' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} + +jobs: + checks: + name: Fast checks + runs-on: ubuntu-24.04 + timeout-minutes: 15 + outputs: + android: ${{ steps.select.outputs.android }} + android_configuration: ${{ steps.select.outputs.android_configuration }} + ios: ${{ steps.select.outputs.ios }} + macos: ${{ steps.select.outputs.macos }} + ios_pods: ${{ steps.pods.outputs.ios_pods }} + macos_pods: ${{ steps.pods.outputs.macos_pods }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - name: Select affected checks + id: select + run: node scripts/ci.ts + env: + CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} + CI_FULL: ${{ inputs.full || github.event_name == 'schedule' }} + - name: Test CI routing + run: | + node --test scripts/ci.test.ts + ruby scripts/ci-pod-lockfiles.test.rb + - uses: oven-sh/setup-bun@v2 + if: steps.select.outputs.dependencies == 'true' + with: + bun-version: 1.3.1 + - name: Refresh dependency update lockfile + if: steps.select.outputs.dependencies == 'true' && github.event.pull_request.user.login == 'dependabot[bot]' + run: bun install --lockfile-only --ignore-scripts + - name: Install dependencies + if: steps.select.outputs.dependencies == 'true' + run: bun install --frozen-lockfile + - name: Synchronize workspace lockfile metadata + if: steps.select.outputs.dependencies == 'true' + run: bun run sync:lockfile + - name: Attach prepared lockfiles + if: always() && steps.select.outputs.dependencies == 'true' + uses: actions/upload-artifact@v4 + with: + name: ci-lockfiles + path: | + bun.lock + example/ios/Podfile.lock + example/macos/Podfile.lock + retention-days: 1 + - name: Require committed dependency resolutions + if: steps.select.outputs.dependencies == 'true' + run: | + bun run check:lockfile + git diff --exit-code -- bun.lock + - name: Select native lockfile checks + id: pods + if: steps.select.outputs.dependencies == 'true' && (steps.select.outputs.ios == 'true' || steps.select.outputs.macos == 'true') + run: node scripts/ci-dependencies.ts + env: + CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} + CI_FULL: ${{ inputs.full || github.event_name == 'schedule' }} + - name: Test dependency routing and release preflight + if: steps.select.outputs.dependencies == 'true' + run: | + node --test scripts/ci-dependencies.test.ts + bun run test:release + - name: Compile TypeScript + if: steps.select.outputs.typescript == 'true' + run: bun typecheck + - name: Lint TypeScript + if: steps.select.outputs.typescript == 'true' + run: | + bun --cwd example lint + bun --cwd packages/react-native-nitro-sqlite lint + git diff --exit-code HEAD -- . ':(exclude)bun.lock' + - name: Test JavaScript wrappers and example runners + if: steps.select.outputs.typescript == 'true' + run: | + (cd packages/react-native-nitro-sqlite && ../../node_modules/.bin/jest --coverage --runInBand) + (cd packages/react-native-nitro-sqlite-vec && ../../node_modules/.bin/jest --coverage --runInBand) + (cd example && ../node_modules/.bin/jest --config jest.mocha.config.js --runInBand --watchman=false) + node --test example/macos/scripts/test-macos.test.mjs + - name: Format C++ + if: steps.select.outputs.cpp_format == 'true' + run: | + bash scripts/clang-format.sh + git diff --exit-code HEAD -- . ':(exclude)bun.lock' + - name: Test C++ database lifecycle and SQLite symbols + if: steps.select.outputs.cpp_tests == 'true' + run: bash scripts/test-cpp.sh + - name: Test SQLite thread-safety pod configuration + if: steps.select.outputs.ios == 'true' + run: ruby scripts/test-podspec-threadsafe.rb + - name: Check documentation + if: steps.select.outputs.docs == 'true' + run: | + bun docs typecheck + bun docs build + + lockfiles: + name: Native lockfiles + needs: checks + if: >- + github.event_name != 'push' && !github.event.pull_request.draft && + (needs.checks.outputs.ios_pods == 'true' || needs.checks.outputs.macos_pods == 'true') + uses: ./.github/workflows/update-lockfiles.yml + with: + ios: ${{ needs.checks.outputs.ios_pods == 'true' }} + macos: ${{ needs.checks.outputs.macos_pods == 'true' }} + + android: + name: Android + needs: [checks, lockfiles] + if: >- + !cancelled() && needs.checks.result == 'success' && + (needs.lockfiles.result == 'success' || needs.lockfiles.result == 'skipped') && + github.event_name != 'push' && !github.event.pull_request.draft && + needs.checks.outputs.android == 'true' + uses: ./.github/workflows/test-harness-android.yml + with: + full: ${{ inputs.full || github.event_name == 'schedule' }} + configuration: ${{ needs.checks.outputs.android_configuration == 'true' }} + + ios: + name: iOS + needs: [checks, lockfiles] + if: >- + !cancelled() && needs.checks.result == 'success' && + (needs.lockfiles.result == 'success' || needs.lockfiles.result == 'skipped') && + github.event_name != 'push' && !github.event.pull_request.draft && + needs.checks.outputs.ios == 'true' + uses: ./.github/workflows/test-harness-ios.yml + with: + full: ${{ inputs.full || github.event_name == 'schedule' }} + + macos: + name: macOS + needs: [checks, lockfiles] + if: >- + !cancelled() && needs.checks.result == 'success' && + (needs.lockfiles.result == 'success' || needs.lockfiles.result == 'skipped') && + github.event_name != 'push' && !github.event.pull_request.draft && + needs.checks.outputs.macos == 'true' + uses: ./.github/workflows/test-macos.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 3931e9b5..00000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Documentation - -on: - push: - branches: [main] - paths: - - "docs/**" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/src/**" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "config/tsconfig.json" - - "package.json" - - "bun.lock" - - ".github/workflows/docs.yml" - pull_request: - paths: - - "docs/**" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/src/**" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "config/tsconfig.json" - - "package.json" - - "bun.lock" - - ".github/workflows/docs.yml" - -permissions: - contents: read - -concurrency: - group: docs-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - check: - name: Typecheck and build - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 - with: - node-version: 22 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.3.1 - - name: Install dependencies - run: bun install --frozen-lockfile - - name: Check types - run: bun docs typecheck - - name: Build site - run: bun docs build diff --git a/.github/workflows/lint-cpp.yml b/.github/workflows/lint-cpp.yml deleted file mode 100644 index 2c0b0863..00000000 --- a/.github/workflows/lint-cpp.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Lint C++ - -on: - push: - branches: - - main - paths: &cpp_paths - - ".github/workflows/lint-cpp.yml" - - ".clang-format" - - "scripts/clang-format.sh" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/tests/cpp/**" - pull_request: - paths: *cpp_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - lint: - name: Format C++ - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - - - name: Run clang-format - run: bun run lint-cpp - - - name: Verify no files have changed after format - run: git diff --exit-code HEAD -- . ':(exclude)bun.lock' diff --git a/.github/workflows/lint-typescript.yml b/.github/workflows/lint-typescript.yml deleted file mode 100644 index 0002c24f..00000000 --- a/.github/workflows/lint-typescript.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Lint TypeScript - -permissions: - checks: write - contents: read - pull-requests: read - -on: - push: - branches: - - main - paths: &typescript_paths - - ".github/workflows/lint-typescript.yml" - - ".prettierrc.js" - - "config/**" - - "tsconfig.json" - - "package.json" - - "bun.lock" - - "patches/**" - - "example/**/*.ts" - - "example/**/*.tsx" - - "example/**/*.js" - - "example/**/*.jsx" - - "example/**/*.mjs" - - "example/tsconfig.json" - - "example/app.json" - - "example/package.json" - - "example/macos/package.json" - - "packages/react-native-nitro-sqlite/**/*.ts" - - "packages/react-native-nitro-sqlite/**/*.tsx" - - "packages/react-native-nitro-sqlite/**/*.js" - - "packages/react-native-nitro-sqlite/**/*.jsx" - - "packages/react-native-nitro-sqlite/tsconfig.json" - - "packages/react-native-nitro-sqlite/tsconfig.build.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite/nitro.json" - - "packages/react-native-nitro-sqlite-vec/**/*.ts" - - "packages/react-native-nitro-sqlite-vec/**/*.tsx" - - "packages/react-native-nitro-sqlite-vec/**/*.js" - - "packages/react-native-nitro-sqlite-vec/**/*.jsx" - - "packages/react-native-nitro-sqlite-vec/tsconfig.json" - - "packages/react-native-nitro-sqlite-vec/package.json" - pull_request: - paths: *typescript_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - tsc: - name: Compile TypeScript (tsc) - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - - uses: reviewdog/action-setup@v1 - - - name: Install npm dependencies (bun) - run: bun install - - - name: Run TypeScript (tsc) - run: | - set -o pipefail - bun typecheck | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-level=error -tee - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - lint: - name: Lint and test TypeScript - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - - - name: Install npm dependencies (bun) - run: bun install - - - name: Run ESLint with auto-fix in example - working-directory: example - run: bun lint - - name: Run ESLint with auto-fix in react-native-nitro-sqlite - working-directory: packages/react-native-nitro-sqlite - run: bun lint - - - name: Verify no files have changed after auto-fix - run: git diff --exit-code HEAD -- . ':(exclude)bun.lock' - - - name: Test core JavaScript wrappers - working-directory: packages/react-native-nitro-sqlite - run: ../../node_modules/.bin/jest --coverage --runInBand - - - name: Test sqlite-vec helpers - working-directory: packages/react-native-nitro-sqlite-vec - run: ../../node_modules/.bin/jest --coverage --runInBand - - - name: Test the in-app Mocha runner - working-directory: example - run: ../node_modules/.bin/jest --config jest.mocha.config.js --runInBand --watchman=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d89ee9f1..70175143 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,8 +18,17 @@ concurrency: cancel-in-progress: false jobs: + preflight: + name: Release compatibility checks + permissions: + contents: read + uses: ./.github/workflows/ci.yml + with: + full: true + release: name: Publish packages and GitHub release + needs: preflight runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/test-cpp.yml b/.github/workflows/test-cpp.yml deleted file mode 100644 index fe86430c..00000000 --- a/.github/workflows/test-cpp.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Test C++ - -on: - push: - branches: - - main - paths: &cpp_test_paths - - ".github/workflows/test-cpp.yml" - - "packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.*" - - "packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.*" - - "packages/react-native-nitro-sqlite/cpp/NitroSQLiteExecuteBatch.*" - - "packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.*" - - "packages/react-native-nitro-sqlite/cpp/NitroSQLiteStatementGroup.hpp" - - "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*" - - "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3-symbol-prefix.h" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "packages/react-native-nitro-sqlite/tests/cpp/**" - - "scripts/generate-sqlite-symbol-prefix.py" - - "scripts/test-private-sqlite-symbols.sh" - pull_request: - paths: *cpp_test_paths - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - test: - name: Database lifecycle tests - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - - name: Check private SQLite symbols and coexistence - run: | - python3 scripts/generate-sqlite-symbol-prefix.py --check - sh scripts/test-private-sqlite-symbols.sh - - - name: Build bundled SQLite - run: | - clang \ - -std=c11 \ - -DSQLITE_THREADSAFE=2 \ - -c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \ - -o /tmp/sqlite3.o - - - name: Build migration tests - run: | - clang++ \ - -std=c++20 \ - -Wall \ - -Wextra \ - -Werror \ - -Ipackages/react-native-nitro-sqlite/cpp \ - -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ - packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ - packages/react-native-nitro-sqlite/tests/cpp/databaseMigration.test.cpp \ - /tmp/sqlite3.o \ - -ldl \ - -lm \ - -pthread \ - -o /tmp/databaseMigrationTests - - - name: Run migration tests - run: /tmp/databaseMigrationTests - - - name: Build connection tests - run: | - clang++ \ - -std=c++20 \ - -Wall \ - -Wextra \ - -Werror \ - -Ipackages/react-native-nitro-sqlite/cpp \ - -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ - packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp \ - packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ - packages/react-native-nitro-sqlite/tests/cpp/databaseConnections.test.cpp \ - /tmp/sqlite3.o \ - -ldl \ - -lm \ - -pthread \ - -o /tmp/databaseConnectionsTests - - - name: Run connection tests - run: /tmp/databaseConnectionsTests - - - name: Build and run statement group tests - run: | - clang++ \ - -std=c++20 \ - -Wall \ - -Wextra \ - -Werror \ - -Ipackages/react-native-nitro-sqlite/cpp \ - -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ - packages/react-native-nitro-sqlite/tests/cpp/statementGroup.test.cpp \ - /tmp/sqlite3.o \ - -ldl \ - -lm \ - -pthread \ - -o /tmp/statementGroupTests - /tmp/statementGroupTests - - - name: Build and run thread safety gate test - run: | - clang \ - -std=c11 \ - -DSQLITE_THREADSAFE=0 \ - -c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \ - -o /tmp/sqlite3-threadsafe-0.o - clang++ \ - -std=c++20 \ - -Wall \ - -Wextra \ - -Werror \ - -Ipackages/react-native-nitro-sqlite/cpp \ - -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ - packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp \ - packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ - packages/react-native-nitro-sqlite/tests/cpp/databaseConnectionsThreadsafe.test.cpp \ - /tmp/sqlite3-threadsafe-0.o \ - -ldl \ - -lm \ - -pthread \ - -o /tmp/databaseConnectionsThreadsafeTests - /tmp/databaseConnectionsThreadsafeTests diff --git a/.github/workflows/test-harness-android.yml b/.github/workflows/test-harness-android.yml index 45eb1384..b5a6278d 100644 --- a/.github/workflows/test-harness-android.yml +++ b/.github/workflows/test-harness-android.yml @@ -1,71 +1,84 @@ -name: Test Harness Android +name: Build and test Android on: - workflow_dispatch: - pull_request: - paths: - - ".github/workflows/test-harness-android.yml" - - "example/android/**" - - "example/src/tests/**" - - "example/tests/**" - - "example/rn-harness.config.mjs" - - "example/jest.config.js" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/android/**" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/android/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "bun.lock" - - "package.json" - - "example/package.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/react-native-nitro-modules*" - - "example/react-native.config.js" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - - "!packages/react-native-nitro-sqlite/src/__tests__/**" + workflow_call: + inputs: + full: + type: boolean + default: false + configuration: + type: boolean + default: false -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true +permissions: + contents: read env: TARGET_ANDROID_ARCH: x86_64 jobs: test: - name: Harness Tests (Android) - runs-on: ubuntu-latest - + name: Android integration tests, minSdk ${{ matrix.min_sdk }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + matrix: + min_sdk: ${{ fromJSON(inputs.full && '["24", "26"]' || '["24"]') }} steps: + - name: Enable KVM group permissions + 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 - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + name: ci-lockfiles + path: . - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 + + - name: Install npm dependencies (bun) + run: bun install --frozen-lockfile + + - name: Set minSdk in build.gradle + run: sed -i "s/minSdkVersion = 24/minSdkVersion = ${{ matrix.min_sdk }}/g" example/android/build.gradle - name: Setup JDK 17 uses: actions/setup-java@v6 with: distribution: "zulu" java-version: 17 + java-package: jdk - - 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: Restore Gradle dependencies + uses: actions/cache@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-gradle-v2- + - name: Test Android SQLite build flags + run: example/android/gradlew --project-dir scripts/android-sqlite-flags testSqliteFlags --no-daemon - - name: Install dependencies (bun) - run: bun install + - name: Check Gradle configuration cache + if: matrix.min_sdk == '24' && (inputs.full || inputs.configuration) + working-directory: example/android + run: | + # React Native creates the autolinking file on the first run. + ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon + ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon + ./gradlew :react-native-nitro-sqlite:help --configuration-cache --no-daemon | tee "$RUNNER_TEMP/nitro-sqlite-configuration-cache.log" + grep -q 'Configuration cache entry reused' "$RUNNER_TEMP/nitro-sqlite-configuration-cache.log" - name: Build Android app working-directory: example/android - run: >- - ./gradlew :app:assembleDebug --no-daemon - -PreactNativeArchitectures=${{ env.TARGET_ANDROID_ARCH }} + run: ./gradlew :app:assembleDebug --no-daemon --build-cache -PreactNativeArchitectures=${{ env.TARGET_ANDROID_ARCH }} - - name: Run react-native-harness on Android + - name: Run Android integration tests uses: callstackincubator/react-native-harness@v1.5.0 with: app: android/app/build/outputs/apk/debug/app-debug.apk @@ -73,3 +86,7 @@ jobs: projectRoot: example packageManager: bun cacheAvd: false + - name: Stop Gradle Daemon + if: always() + working-directory: example/android + run: ./gradlew --stop diff --git a/.github/workflows/test-harness-ios.yml b/.github/workflows/test-harness-ios.yml index e73d98b1..ba48808e 100644 --- a/.github/workflows/test-harness-ios.yml +++ b/.github/workflows/test-harness-ios.yml @@ -1,54 +1,54 @@ -name: Test Harness iOS +name: Build and test iOS on: - workflow_dispatch: - pull_request: - paths: - - ".github/workflows/test-harness-ios.yml" - - "example/ios/**" - - "example/src/tests/**" - - "example/tests/**" - - "example/rn-harness.config.mjs" - - "example/jest.config.js" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/ios/**" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec" - - "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec" - - "example/Gemfile.lock" - - "bun.lock" - - "package.json" - - "example/package.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/react-native-nitro-modules*" - - "example/react-native.config.js" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - - "!packages/react-native-nitro-sqlite/src/__tests__/**" + workflow_call: + inputs: + full: + type: boolean + default: false -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true +permissions: + contents: read jobs: test: - name: Harness Tests (iOS) - runs-on: macOS-26 + name: iOS integration tests, ${{ matrix.config.name }}, Xcode ${{ matrix.config.xcode }} + runs-on: macos-26 + timeout-minutes: 40 + strategy: + matrix: + config: ${{ fromJSON(inputs.full && '[{"name":"static_frameworks","frameworks":"static","xcode":"26.5"},{"name":"no_frameworks","frameworks":"","xcode":"26.5"},{"name":"static_frameworks","frameworks":"static","xcode":"26.2"}]' || '[{"name":"static_frameworks","frameworks":"static","xcode":"26.5"},{"name":"no_frameworks","frameworks":"","xcode":"26.5"}]') }} env: + USE_CCACHE: 1 + USE_FRAMEWORKS: ${{ matrix.config.frameworks }} IOS_SIMULATOR_NAME: iPhone 17 Pro - IOS_SIMULATOR_OS: "26.2" - + IOS_SIMULATOR_OS: ${{ matrix.config.xcode }} steps: - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + name: ci-lockfiles + path: . + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 + + - name: Install npm dependencies (bun) + run: bun install --frozen-lockfile - - name: Install dependencies (bun) - run: bun install + - name: Install Ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + max-size: 500M + key: ${{ runner.os }}-${{ matrix.config.name }}-ccache-example-ios-xcode${{ matrix.config.xcode }} + create-symlink: true + - name: Setup ccache behavior + run: | + echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV + echo "CCACHE_FILECLONE=true" >> $GITHUB_ENV + echo "CCACHE_DEPEND=true" >> $GITHUB_ENV + echo "CCACHE_INODECACHE=true" >> $GITHUB_ENV - name: Setup Ruby (bundle) uses: ruby/setup-ruby@v1 @@ -57,33 +57,40 @@ jobs: bundler-cache: true working-directory: example - - name: Select Xcode 26.2 - run: sudo xcode-select -s "/Applications/Xcode_26.2.app/Contents/Developer" + - name: Select Xcode ${{ matrix.config.xcode }} + run: sudo xcode-select -s "/Applications/Xcode_${{ matrix.config.xcode }}.app/Contents/Developer" + - name: Restore Pods cache + uses: actions/cache@v6 + with: + path: example/ios/Pods + key: ${{ runner.os }}-${{ matrix.config.name }}-pods-v2-${{ hashFiles('**/Podfile.lock', '**/Gemfile.lock') }} + - name: Clean generated iOS codegen + run: rm -rf example/ios/build/generated - name: Install Pods working-directory: example run: bun pods - - name: Build debug app + - name: Build App working-directory: example/ios run: | - set -euo pipefail - + set -o pipefail xcodebuild \ CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \ - -derivedDataPath build -UseModernBuildSystem=YES \ + -derivedDataPath build/DerivedData -UseModernBuildSystem=YES \ -workspace NitroSQLiteExample.xcworkspace \ -scheme NitroSQLiteExample \ -sdk iphonesimulator \ -configuration Debug \ - -destination "generic/platform=iOS Simulator" \ + -destination 'generic/platform=iOS Simulator' \ + -showBuildTimingSummary \ + ONLY_ACTIVE_ARCH=YES \ build \ - CODE_SIGNING_ALLOWED=NO - - - name: Run react-native-harness on iOS + CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions + - name: Run iOS integration tests uses: callstackincubator/react-native-harness@v1.5.0 with: - app: ios/build/Build/Products/Debug-iphonesimulator/NitroSQLiteExample.app + app: ios/build/DerivedData/Build/Products/Debug-iphonesimulator/NitroSQLiteExample.app runner: ios projectRoot: example packageManager: bun diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 3f6ae4be..c549fe28 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -1,60 +1,46 @@ -name: Test macOS +name: Build and test macOS on: - workflow_dispatch: - pull_request: - paths: - - ".github/workflows/test-macos.yml" - - "example/macos/**" - - "example/src/**" - - "example/tests/**" - - "example/app.json" - - "example/index.js" - - "example/babel.config*.js" - - "packages/react-native-nitro-sqlite/src/**" - - "packages/react-native-nitro-sqlite/cpp/**" - - "packages/react-native-nitro-sqlite/ios/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/shared/**" - - "packages/react-native-nitro-sqlite/nitrogen/generated/ios/**" - - "packages/react-native-nitro-sqlite/RNNitroSQLite.podspec" - - "packages/react-native-nitro-sqlite/react-native.config.js" - - "packages/react-native-nitro-sqlite/nitro.json" - - "packages/react-native-nitro-sqlite/package.json" - - "packages/react-native-nitro-sqlite-vec/src/**" - - "packages/react-native-nitro-sqlite-vec/cpp/**" - - "packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec" - - "packages/react-native-nitro-sqlite-vec/react-native.config.js" - - "packages/react-native-nitro-sqlite-vec/package.json" - - "patches/**" - - "example/macos/Podfile.lock" - - "example/Gemfile.lock" - - "bun.lock" - - "package.json" - - "example/package.json" - - "example/macos/package.json" - - "example/react-native.config.js" - - "example/macos/react-native.config.js" - - "!packages/react-native-nitro-sqlite/src/__tests__/**" - - "!packages/react-native-nitro-sqlite-vec/src/__tests__/**" + workflow_call: -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true +permissions: + contents: read + +env: + USE_CCACHE: 1 jobs: test: - name: macOS Integration Tests + name: macOS integration tests runs-on: macos-26 - + timeout-minutes: 30 steps: - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + name: ci-lockfiles + path: . - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.1 - - name: Install dependencies (bun) - run: bun install + - name: Install npm dependencies (bun) + run: bun install --frozen-lockfile - - name: Test desktop runner failure handling - run: node --test example/macos/scripts/test-macos.test.mjs + - name: Install Ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + max-size: 500M + key: ${{ runner.os }}-ccache-example-macos + create-symlink: true + - name: Setup ccache behavior + run: | + { + echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" + echo "CCACHE_FILECLONE=true" + echo "CCACHE_DEPEND=true" + echo "CCACHE_INODECACHE=true" + } >> "$GITHUB_ENV" - name: Setup Ruby (bundle) uses: ruby/setup-ruby@v1 @@ -66,22 +52,34 @@ jobs: - name: Select Xcode 26.5 run: sudo xcode-select -s "/Applications/Xcode_26.5.app/Contents/Developer" + - name: Restore Pods cache + uses: actions/cache@v6 + with: + path: example/macos/Pods + key: ${{ runner.os }}-pods-macos-${{ hashFiles('example/macos/Podfile.lock', 'example/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods-macos- + - name: Clean generated macOS codegen + run: rm -rf example/macos/build/generated - name: Install Pods run: bun --cwd example pods:macos - - name: Build debug app + - name: Build App working-directory: example/macos run: | - set -euo pipefail + set -o pipefail xcodebuild \ - -derivedDataPath build -UseModernBuildSystem=YES \ + -derivedDataPath build/DerivedData -UseModernBuildSystem=YES \ -workspace NitroSQLiteExample.xcworkspace \ -scheme NitroSQLiteExample-macOS \ -configuration Debug \ -destination 'platform=macOS,arch=arm64' \ + -showBuildTimingSummary \ + ONLY_ACTIVE_ARCH=YES \ build \ - CODE_SIGNING_ALLOWED=NO - + CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions - name: Run macOS integration tests working-directory: example/macos + env: + MACOS_APP_PATH: build/DerivedData/Build/Products/Debug/NitroSQLiteExample.app run: bun run test diff --git a/.github/workflows/update-lockfiles.yml b/.github/workflows/update-lockfiles.yml index 4431a1ea..f60ea073 100644 --- a/.github/workflows/update-lockfiles.yml +++ b/.github/workflows/update-lockfiles.yml @@ -1,59 +1,68 @@ -name: "Update Lockfiles (bun.lock + Podfile.lock)" +name: Check native lockfiles on: - pull_request: - branches: - - main - paths: - - "example/package.json" - - "packages/react-native-nitro-sqlite/package.json" + workflow_call: + inputs: + ios: + type: boolean + default: false + macos: + type: boolean + default: false permissions: - contents: write + contents: read jobs: - update-lockfiles: - name: "Update lockfiles (Podfile.lock)" - if: github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository + check: + name: Check CocoaPods dependency resolutions runs-on: macos-26 + timeout-minutes: 15 + env: + USE_FRAMEWORKS: static steps: - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.ref }} - + name: ci-lockfiles + path: . - uses: oven-sh/setup-bun@v2 - - - name: Setup Ruby (bundle) - uses: ruby/setup-ruby@v1 + with: + bun-version: 1.3.1 + - name: Install dependencies + run: bun install --frozen-lockfile + - uses: ruby/setup-ruby@v1 with: ruby-version: 3.3.0 bundler-cache: true working-directory: example - - name: Select Xcode 26.5 - run: sudo xcode-select -s "/Applications/Xcode_26.5.app/Contents/Developer" - - - run: | - bun install - - cd example - bundle install - bun pods - bun pods:macos - cd .. - - git add bun.lock - git add example/ios/Podfile.lock - git add example/macos/Podfile.lock - git add example/Gemfile.lock - - if git diff --cached --quiet; then - echo "Lockfiles are already up to date" - exit 0 - fi - - git config --global user.name 'dependabot[bot]' - git config --global user.email 'dependabot[bot]@users.noreply.github.com' - git commit --amend --no-edit - git push --force + run: sudo xcode-select -s '/Applications/Xcode_26.5.app/Contents/Developer' + - name: Refresh iOS lockfile + if: inputs.ios + working-directory: example + run: bun pods + - name: Refresh macOS lockfile + if: inputs.macos + working-directory: example + run: bun pods:macos + - name: Attach refreshed native lockfiles + if: always() + uses: actions/upload-artifact@v4 + with: + name: native-lockfiles + path: | + example/ios/Podfile.lock + example/macos/Podfile.lock + example/Gemfile.lock + retention-days: 1 + - name: Require committed native dependency resolutions + env: + CHECK_IOS: ${{ inputs.ios }} + CHECK_MACOS: ${{ inputs.macos }} + run: | + lockfiles=() + if [[ "$CHECK_IOS" == true ]]; then lockfiles+=(example/ios/Podfile.lock); fi + if [[ "$CHECK_MACOS" == true ]]; then lockfiles+=(example/macos/Podfile.lock); fi + ruby scripts/ci-pod-lockfiles.rb "${lockfiles[@]}" + git diff --exit-code -- example/Gemfile.lock diff --git a/example/ios/Podfile b/example/ios/Podfile index 7af75b28..4112c136 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,6 @@ ENV['RCT_NEW_ARCH_ENABLED'] = '1' -ENV['USE_FRAMEWORKS'] = "static" +# Default to static frameworks locally, while allowing CI to select either mode. +ENV['USE_FRAMEWORKS'] ||= 'static' ENV['NITRO_SQLITE_VEC'] = '1' # Resolve react_native_pods.rb with node to allow for hoisting @@ -13,10 +14,10 @@ platform :ios, min_ios_version_supported prepare_react_native_project! linkage = ENV['USE_FRAMEWORKS'] - if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym - end +unless linkage.to_s.empty? + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end target 'NitroSQLiteExample' do config = use_native_modules! diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 06626e50..f766b1f8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2222,12 +2222,12 @@ SPEC CHECKSUMS: ReactCommon: 749169e12aa3461a3df3656a00e48dbe0456b2d6 ReactNativeDependencies: 94c922c39e0e6150ee9ad05e2ca1946e0ef55783 RNCClipboard: e706ef7ccbbfd69f829c1b683f893bc7668c9fdb - RNNitroSQLite: 119ba2dae35083ed5306594ad99cf5d4d7e100c0 - RNNitroSqliteVec: 0c255fb910dbcea6c77ab22a9b9b2e10cefdc7f0 + RNNitroSQLite: 6239f0b82c1888b4ced7601da40e9d5c2ee57a91 + RNNitroSqliteVec: 40011b65949eab9ea5d965c0ca3cf2f9f274c072 RNScreens: 3c977859fd13e67dc4ebfc24afb4d7c2e894db61 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e -PODFILE CHECKSUM: 6933dbd8b4eea93df54a57c95e03351734e37a5b +PODFILE CHECKSUM: fd5bc9f335e919fdcd0b0eaf293b8e68d12e6ed4 COCOAPODS: 1.15.2 diff --git a/scripts/ci-dependencies.test.ts b/scripts/ci-dependencies.test.ts new file mode 100644 index 00000000..d9ae9676 --- /dev/null +++ b/scripts/ci-dependencies.test.ts @@ -0,0 +1,157 @@ +import assert from 'node:assert/strict' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import test from 'node:test' +import { + nativeDependenciesChanged, + selectPodChecks, +} from './ci-dependencies.ts' + +test('JavaScript-only dependency changes skip CocoaPods', () => { + const root = packageFixture('formatter') + try { + assert.equal( + nativeDependenciesChanged( + { formatter: ['formatter@1.0.0'] }, + { formatter: ['formatter@1.1.0'] }, + root, + ), + false, + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test('native package updates and additions require CocoaPods', () => { + const root = packageFixture('@vendor/storage', 'ios/Storage.podspec') + try { + assert.equal( + nativeDependenciesChanged( + { storage: ['@vendor/storage@1.0.0'] }, + { storage: ['@vendor/storage@1.1.0'] }, + root, + ), + true, + ) + assert.equal( + nativeDependenciesChanged( + {}, + { storage: ['@vendor/storage@1.1.0'] }, + root, + ), + true, + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test('native packages in isolated macOS dependencies are detected', () => { + const root = packageFixture( + 'desktop-storage', + 'Storage.podspec', + 'example/macos', + ) + try { + assert.equal( + nativeDependenciesChanged( + { storage: ['desktop-storage@1.0.0'] }, + { storage: ['desktop-storage@2.0.0'] }, + root, + ), + true, + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test('unchanged native resolutions do not reinstall Pods for JavaScript metadata changes', () => { + assert.equal( + nativeDependenciesChanged( + { native: ['react-native@0.87.1', { old: true }] }, + { native: ['react-native@0.87.1', { new: true }] }, + '/unused', + ), + false, + ) +}) + +test('removed packages conservatively require native lockfile validation', () => { + assert.equal( + nativeDependenciesChanged( + { old: ['removed-package@1.0.0'] }, + {}, + '/missing', + ), + true, + ) +}) + +test('Expo modules require CocoaPods without a root podspec', () => { + const root = packageFixture('expo-module', 'expo-module.config.json') + try { + assert.equal( + nativeDependenciesChanged( + { expo: ['expo-module@1.0.0'] }, + { expo: ['expo-module@1.0.1'] }, + root, + ), + true, + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test('invalid locked package records fail rather than silently skipping validation', () => { + assert.throws( + () => nativeDependenciesChanged({ broken: null }, {}, '/unused'), + /string resolution/, + ) +}) + +test('Apple configuration changes only prepare the affected lockfile', () => { + assert.deepEqual(selectPodChecks(['example/ios/Podfile'], false, false), { + ios: true, + macos: false, + }) + assert.deepEqual(selectPodChecks(['example/macos/Podfile'], false, false), { + ios: false, + macos: true, + }) + assert.deepEqual(selectPodChecks(['example/package.json'], false, false), { + ios: false, + macos: false, + }) + assert.deepEqual( + selectPodChecks( + ['packages/react-native-nitro-sqlite/RNNitroSQLite.podspec'], + false, + false, + ), + { ios: true, macos: true }, + ) +}) + +test('native dependency changes and full runs prepare both Apple lockfiles', () => { + assert.deepEqual(selectPodChecks([], true, false), { ios: true, macos: true }) + assert.deepEqual(selectPodChecks([], false, true), { ios: true, macos: true }) +}) + +function packageFixture( + name: string, + nativeFile?: string, + workspace = '', +): string { + const root = mkdtempSync(join(tmpdir(), 'nitro-ci-dependencies-')) + const directory = join(root, workspace, 'node_modules', name) + mkdirSync(join(directory, 'ios'), { recursive: true }) + writeFileSync(join(directory, 'package.json'), '{}') + if (nativeFile) { + writeFileSync(join(directory, nativeFile), '') + } + return root +} diff --git a/scripts/ci-dependencies.ts b/scripts/ci-dependencies.ts new file mode 100644 index 00000000..4eb4469d --- /dev/null +++ b/scripts/ci-dependencies.ts @@ -0,0 +1,157 @@ +import { execFileSync } from 'node:child_process' +import { appendFileSync, existsSync, readdirSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import ts from 'typescript' +import { readLockfile, requireRecord } from './bun-lockfile-workspaces.ts' +import { matchesPaths } from './ci.ts' + +/** + * Detects native dependency changes by comparing locked package resolutions. + * Packages missing from the installed tree conservatively require Pods. + * @param before The base lockfile's packages table. + * @param after The current lockfile's packages table. + * @param root The repository root containing the installed workspaces. + * @returns Whether a changed package may affect native dependency resolution. + * @throws If a locked package has an invalid resolution entry. + */ +export function nativeDependenciesChanged( + before: Record, + after: Record, + root: string, +): boolean { + const names = new Set([...Object.keys(before), ...Object.keys(after)]) + return [...names].some((key) => { + const previous = resolution(before[key]) + const current = resolution(after[key]) + if (previous === current) { + return false + } + const packageName = (current || previous).slice( + 0, + (current || previous).lastIndexOf('@'), + ) + return isNativePackage(packageName, root) + }) +} + +/** + * Selects Apple lockfiles after native dependencies or CocoaPods inputs change. + * @param files Repository-relative paths changed since the base commit. + * @param nativeChanged Whether a locked native package changed. + * @param full Whether to validate both Apple platforms unconditionally. + * @returns Independent iOS and macOS lockfile selections. + */ +export function selectPodChecks( + files: string[], + nativeChanged: boolean, + full: boolean, +) { + const shared = [ + '**/*.podspec', + '**/react-native.config.js', + 'example/Gemfile*', + 'patches/**', + ] + return { + ios: + full || + nativeChanged || + files.some((file) => + matchesPaths(file, [...shared, 'example/ios/Podfile']), + ), + macos: + full || + nativeChanged || + files.some((file) => + matchesPaths(file, [ + ...shared, + 'example/macos/Podfile', + 'example/macos/scripts/*config*.js', + 'example/macos/scripts/resolve-react-native.js', + ]), + ), + } +} + +function main(): void { + const base = process.env.CI_BASE_SHA + const full = process.env.CI_FULL === 'true' || !base || /^0+$/.test(base) + const current = readLockfile(process.cwd()) + let before = current + let files: string[] = [] + if (!full && base) { + const content = execFileSync('git', ['show', `${base}:bun.lock`], { + encoding: 'utf8', + }) + const { config, error } = ts.parseConfigFileTextToJson('bun.lock', content) + if (error) { + throw new Error(ts.flattenDiagnosticMessageText(error.messageText, '\n')) + } + before = requireRecord(config, 'base lockfile') + files = execFileSync('git', ['diff', '--name-only', '-z', base, 'HEAD'], { + encoding: 'utf8', + }) + .split('\0') + .filter(Boolean) + } + const pods = selectPodChecks( + files, + nativeDependenciesChanged( + requireRecord(before.packages, 'base packages'), + requireRecord(current.packages, 'current packages'), + process.cwd(), + ), + full, + ) + const output = process.env.GITHUB_OUTPUT + if (!output) { + throw new Error('GITHUB_OUTPUT is required') + } + appendFileSync(output, `ios_pods=${pods.ios}\nmacos_pods=${pods.macos}\n`) +} + +function resolution(value: unknown): string { + if (value === undefined) { + return '' + } + if (!Array.isArray(value) || typeof value[0] !== 'string') { + throw new Error('A locked package must have a string resolution') + } + return value[0] +} + +function isNativePackage(name: string, root: string): boolean { + if (/(^|\/)react-native($|-)/.test(name)) { + return true + } + const directories = [ + '', + 'example', + 'example/macos', + 'packages/react-native-nitro-sqlite', + 'packages/react-native-nitro-sqlite-vec', + ] + .map((workspace) => join(root, workspace, 'node_modules', name)) + .filter((directory) => existsSync(join(directory, 'package.json'))) + if (directories.length === 0) { + return true + } + return directories.some((directory) => + [directory, join(directory, 'ios')].some( + (location) => + existsSync(location) && + readdirSync(location).some( + (file) => + file.endsWith('.podspec') || file === 'expo-module.config.json', + ), + ), + ) +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + main() +} diff --git a/scripts/ci-pod-lockfiles.rb b/scripts/ci-pod-lockfiles.rb new file mode 100644 index 00000000..63f1d7b6 --- /dev/null +++ b/scripts/ci-pod-lockfiles.rb @@ -0,0 +1,33 @@ +require 'open3' +require 'yaml' + +# Compares installed lockfiles with the committed dependency graph and sources. +# Local podspec checksums can include absolute checkout paths, so they are excluded. +def check_pod_lockfiles(paths) + paths.all? do |path| + committed, status = Open3.capture2('git', 'show', "HEAD:#{path}") + raise "Cannot read committed #{path}" unless status.success? + + if normalize_pod_lockfile(committed) == normalize_pod_lockfile(File.read(path)) + puts "#{path}: dependency resolutions match" + next true + end + + warn "#{path}: dependency resolutions changed. Commit the updated native-lockfiles artifact." + false + end +end + +def normalize_pod_lockfile(content) + lockfile = YAML.safe_load(content, permitted_classes: [Symbol]) + checksums = lockfile.fetch('SPEC CHECKSUMS') + lockfile.fetch('EXTERNAL SOURCES', {}).each do |name, source| + checksums.delete(name) if source.key?(:path) + end + lockfile +end + +if __FILE__ == $PROGRAM_NAME + raise 'At least one Podfile.lock path is required' if ARGV.empty? + exit(check_pod_lockfiles(ARGV) ? 0 : 1) +end diff --git a/scripts/ci-pod-lockfiles.test.rb b/scripts/ci-pod-lockfiles.test.rb new file mode 100644 index 00000000..a0099a31 --- /dev/null +++ b/scripts/ci-pod-lockfiles.test.rb @@ -0,0 +1,43 @@ +require 'minitest/autorun' +require_relative 'ci-pod-lockfiles' + +class PodLockfilesTest < Minitest::Test + LOCKFILE = <<~YAML + PODS: + - LocalPod (1.0.0) + - RemotePod (2.0.0) + DEPENDENCIES: + - LocalPod (from `../packages/local`) + - RemotePod + EXTERNAL SOURCES: + LocalPod: + :path: ../packages/local + SPEC CHECKSUMS: + LocalPod: local-checkout-dependent + RemotePod: remote-checksum + PODFILE CHECKSUM: podfile-checksum + COCOAPODS: 1.15.2 + YAML + + def test_local_pod_checksums_may_differ_between_checkouts + assert_equal normalize_pod_lockfile(LOCKFILE), + normalize_pod_lockfile(LOCKFILE.sub('local-checkout-dependent', 'other-checkout')) + end + + def test_remote_pod_checksums_must_match + refute_equal normalize_pod_lockfile(LOCKFILE), + normalize_pod_lockfile(LOCKFILE.sub('remote-checksum', 'changed-checksum')) + end + + def test_dependency_versions_must_match + refute_equal normalize_pod_lockfile(LOCKFILE), + normalize_pod_lockfile(LOCKFILE.sub('RemotePod (2.0.0)', 'RemotePod (2.1.0)')) + end + + def test_local_sources_and_podfile_checksum_must_match + ['../packages/local', 'podfile-checksum'].each do |input| + refute_equal normalize_pod_lockfile(LOCKFILE), + normalize_pod_lockfile(LOCKFILE.gsub(input, 'changed')) + end + end +end diff --git a/scripts/ci.test.ts b/scripts/ci.test.ts new file mode 100644 index 00000000..4a43118b --- /dev/null +++ b/scripts/ci.test.ts @@ -0,0 +1,240 @@ +import assert from 'node:assert/strict' +import { execFileSync, spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import test from 'node:test' +import { fileURLToPath } from 'node:url' +import { matchesPaths, selectChecks } from './ci.ts' + +const root = fileURLToPath(new URL('../', import.meta.url)) + +test('documentation content only selects the documentation build', () => { + const checks = selectChecks(['docs/content/docs/transactions.mdx'], false) + assert.deepEqual(enabled(checks), ['docs']) +}) + +test('platform changes only select their platform', () => { + assert.deepEqual( + enabled(selectChecks(['example/android/app/build.gradle'], false)), + ['android', 'android_configuration'], + ) + assert.deepEqual(enabled(selectChecks(['example/ios/Podfile'], false)), [ + 'ios', + ]) + assert.deepEqual(enabled(selectChecks(['example/macos/Podfile'], false)), [ + 'macos', + ]) +}) + +test('shared native sources retain lifecycle and platform coverage', () => { + const checks = selectChecks( + [ + 'packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp', + ], + false, + ) + assert.deepEqual(enabled(checks), [ + 'cpp_format', + 'cpp_tests', + 'android', + 'ios', + 'macos', + ]) +}) + +test('generated platform bindings do not trigger the other mobile platform', () => { + const checks = selectChecks( + [ + 'packages/react-native-nitro-sqlite/nitrogen/generated/android/JHybridNitroSQLiteSpec.hpp', + ], + false, + ) + assert.equal(checks.android, true) + assert.equal(checks.ios, false) +}) + +test('JavaScript unit tests do not rebuild native apps', () => { + const checks = selectChecks( + ['packages/react-native-nitro-sqlite/src/__tests__/transaction.test.ts'], + false, + ) + assert.equal(checks.typescript, true) + assert.equal(checks.android, false) + assert.equal(checks.ios, false) + assert.equal(checks.macos, false) +}) + +test('in-app tests and sqlite-vec wrappers retain native coverage', () => { + for (const file of [ + 'example/src/tests/connection.test.ts', + 'packages/react-native-nitro-sqlite-vec/src/index.ts', + ]) { + const checks = selectChecks([file], false) + assert.equal(checks.android, true) + assert.equal(checks.ios, true) + assert.equal(checks.macos, true) + } +}) + +test('dependency lockfile changes retain both mobile and desktop coverage', () => { + const checks = selectChecks(['bun.lock'], false) + assert.equal(checks.android, true) + assert.equal(checks.ios, true) + assert.equal(checks.macos, true) +}) + +test('a full run selects every check even without file changes', () => { + assert.ok(Object.values(selectChecks([], true)).every(Boolean)) +}) + +test('ordered exclusions can be followed by explicit inclusions', () => { + assert.equal( + matchesPaths('src/tests/special.ts', [ + 'src/**', + '!src/tests/**', + 'src/tests/special.ts', + ]), + true, + ) + assert.equal( + matchesPaths('src/tests/other.ts', [ + 'src/**', + '!src/tests/**', + 'src/tests/special.ts', + ]), + false, + ) +}) + +test('the CLI routes real Git commits', () => { + const directory = mkdtempSync(join(tmpdir(), 'nitro-ci-routing-')) + try { + execFileSync('git', ['init', '-q'], { cwd: directory }) + writeFileSync(join(directory, 'package.json'), '{}\n') + execFileSync('git', ['add', '.'], { cwd: directory }) + execFileSync( + 'git', + [ + '-c', + 'user.name=CI Test', + '-c', + 'user.email=ci@example.invalid', + '-c', + 'commit.gpgsign=false', + 'commit', + '-qm', + 'chore: create fixture', + ], + { cwd: directory }, + ) + const base = execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: directory, + encoding: 'utf8', + }).trim() + writeFileSync(join(directory, 'package.json'), '{"changed":true}\n') + execFileSync('git', ['add', '.'], { cwd: directory }) + execFileSync( + 'git', + [ + '-c', + 'user.name=CI Test', + '-c', + 'user.email=ci@example.invalid', + '-c', + 'commit.gpgsign=false', + 'commit', + '-qm', + 'chore: update fixture', + ], + { cwd: directory }, + ) + const output = join(directory, 'output') + execFileSync(process.execPath, [join(root, 'scripts/ci.ts')], { + cwd: directory, + env: { + ...process.env, + CI_BASE_SHA: base, + CI_FULL: 'false', + GITHUB_OUTPUT: output, + }, + }) + const values = readFileSync(output, 'utf8') + assert.match(values, /typescript=true\n/) + assert.match(values, /android=true\n/) + assert.match(values, /dependencies=true\n/) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}) + +test('the real iOS Podfile honors both CI matrix modes and retains the local default', () => { + const script = ` + module Pod + class Config + def self.instance = new + def installation_root = '/stub' + end + module Executable + def self.execute_command(*) = '/stub/react_native_pods.rb' + end + module UI + def self.puts(*) = nil + end + end + class String + def green = self + end + def require(*) = true + def min_ios_version_supported = '15.1' + def platform(*) = nil + def prepare_react_native_project! = nil + def use_frameworks!(linkage:) = STDOUT.puts("frameworks=#{linkage}") + def target(*) = yield + def use_native_modules! = { reactNativePath: '/stub' } + def use_react_native!(**) = nil + def post_install(*) = nil + load ARGV.fetch(0) + ` + for (const [mode, expected] of [ + ['static', 'frameworks=static'], + ['', ''], + [undefined, 'frameworks=static'], + ]) { + const env = { ...process.env } + if (mode === undefined) { + delete env.USE_FRAMEWORKS + } else { + env.USE_FRAMEWORKS = mode + } + const result = spawnSync( + 'ruby', + ['-e', script, join(root, 'example/ios/Podfile')], + { env, encoding: 'utf8' }, + ) + assert.equal(result.status, 0, result.stderr) + assert.equal(result.stdout.trim(), expected) + } +}) + +test('the routine iOS matrix retains both framework configurations', () => { + const workflow = readFileSync( + join(root, '.github/workflows/test-harness-ios.yml'), + 'utf8', + ) + const match = workflow.match( + /fromJSON\(inputs.full && '([^']+)' \|\| '([^']+)'\)/, + ) + assert.ok(match?.[2]) + const matrix: unknown = JSON.parse(match[2]) + assert.deepEqual(matrix, [ + { name: 'static_frameworks', frameworks: 'static', xcode: '26.5' }, + { name: 'no_frameworks', frameworks: '', xcode: '26.5' }, + ]) +}) + +function enabled(checks: ReturnType): string[] { + return Object.entries(checks) + .filter(([, selected]) => selected) + .map(([name]) => name) +} diff --git a/scripts/ci.ts b/scripts/ci.ts new file mode 100644 index 00000000..1df79419 --- /dev/null +++ b/scripts/ci.ts @@ -0,0 +1,82 @@ +import { execFileSync } from 'node:child_process' +import { appendFileSync } from 'node:fs' +import { matchesGlob, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import patterns from '../.github/ci-paths.json' with { type: 'json' } + +/** + * Selects affected checks using the repository's shared path filters. + * @param files Repository-relative paths changed since the base commit. + * @param full Whether to enable all checks, regardless of changed paths. + * @returns A boolean for each independently selectable check. + */ +export function selectChecks(files: string[], full: boolean) { + const selected = (paths: string[]) => + full || files.some((file) => matchesPaths(file, paths)) + return { + typescript: selected(patterns.typescript), + cpp_format: selected(patterns.cpp_format), + cpp_tests: selected(patterns.cpp_tests), + docs: selected(patterns.docs), + android: selected(patterns.android), + android_configuration: selected([ + '.github/workflows/**', + 'example/android/**/*.gradle*', + 'packages/react-native-nitro-sqlite/android/**/*.gradle*', + 'scripts/android-sqlite-flags/**', + ]), + ios: selected(patterns.ios), + macos: selected(patterns.macos), + } +} + +/** + * Applies include and exclude globs in order; the last matching rule wins. + * @param file A repository-relative path. + * @param paths Glob patterns, with `!` prefixes for exclusions. + * @returns Whether a matching include remains after applying exclusions. + */ +export function matchesPaths(file: string, paths: string[]): boolean { + return paths.reduce((included, pattern) => { + const excluded = pattern.startsWith('!') + return matchesGlob(file, excluded ? pattern.slice(1) : pattern) + ? !excluded + : included + }, false) +} + +function main(): void { + const base = process.env.CI_BASE_SHA + const full = process.env.CI_FULL === 'true' || !base || /^0+$/.test(base) + const files = full + ? [] + : execFileSync('git', ['diff', '--name-only', '-z', base, 'HEAD'], { + encoding: 'utf8', + }) + .split('\0') + .filter(Boolean) + const checks = selectChecks(files, full) + const output = process.env.GITHUB_OUTPUT + if (!output) { + throw new Error('GITHUB_OUTPUT is required') + } + const dependencies = + checks.typescript || + checks.docs || + checks.android || + checks.ios || + checks.macos + appendFileSync( + output, + Object.entries({ ...checks, dependencies }) + .map(([name, enabled]) => `${name}=${enabled}\n`) + .join(''), + ) +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + main() +} diff --git a/scripts/test-cpp.sh b/scripts/test-cpp.sh new file mode 100644 index 00000000..58afcb12 --- /dev/null +++ b/scripts/test-cpp.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +python3 scripts/generate-sqlite-symbol-prefix.py --check +sh scripts/test-private-sqlite-symbols.sh + +clang \ + -std=c11 \ + -DSQLITE_THREADSAFE=2 \ + -c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \ + -o /tmp/sqlite3.o + +clang++ \ + -std=c++20 \ + -Wall \ + -Wextra \ + -Werror \ + -Ipackages/react-native-nitro-sqlite/cpp \ + -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ + packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ + packages/react-native-nitro-sqlite/tests/cpp/databaseMigration.test.cpp \ + /tmp/sqlite3.o \ + -ldl \ + -lm \ + -pthread \ + -o /tmp/databaseMigrationTests + +/tmp/databaseMigrationTests + +clang++ \ + -std=c++20 \ + -Wall \ + -Wextra \ + -Werror \ + -Ipackages/react-native-nitro-sqlite/cpp \ + -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ + packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp \ + packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ + packages/react-native-nitro-sqlite/tests/cpp/databaseConnections.test.cpp \ + /tmp/sqlite3.o \ + -ldl \ + -lm \ + -pthread \ + -o /tmp/databaseConnectionsTests + +/tmp/databaseConnectionsTests + +clang++ \ + -std=c++20 \ + -Wall \ + -Wextra \ + -Werror \ + -Ipackages/react-native-nitro-sqlite/cpp \ + -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ + packages/react-native-nitro-sqlite/tests/cpp/statementGroup.test.cpp \ + /tmp/sqlite3.o \ + -ldl \ + -lm \ + -pthread \ + -o /tmp/statementGroupTests +/tmp/statementGroupTests + +clang \ + -std=c11 \ + -DSQLITE_THREADSAFE=0 \ + -c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \ + -o /tmp/sqlite3-threadsafe-0.o +clang++ \ + -std=c++20 \ + -Wall \ + -Wextra \ + -Werror \ + -Ipackages/react-native-nitro-sqlite/cpp \ + -Ipackages/react-native-nitro-sqlite/cpp/sqlite \ + packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.cpp \ + packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.cpp \ + packages/react-native-nitro-sqlite/tests/cpp/databaseConnectionsThreadsafe.test.cpp \ + /tmp/sqlite3-threadsafe-0.o \ + -ldl \ + -lm \ + -pthread \ + -o /tmp/databaseConnectionsThreadsafeTests +/tmp/databaseConnectionsThreadsafeTests From 04171e7e2490633ccd044b4eea2028284773019a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 1 Oct 2026 15:58:20 +0200 Subject: [PATCH 2/3] ci: boot iOS simulators before integration builds --- .github/workflows/test-harness-ios.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/test-harness-ios.yml b/.github/workflows/test-harness-ios.yml index ba48808e..285b820b 100644 --- a/.github/workflows/test-harness-ios.yml +++ b/.github/workflows/test-harness-ios.yml @@ -60,6 +60,25 @@ jobs: - name: Select Xcode ${{ matrix.config.xcode }} run: sudo xcode-select -s "/Applications/Xcode_${{ matrix.config.xcode }}.app/Contents/Developer" + - name: Start iOS simulator + run: | + set -euo pipefail + + simulator="$(xcrun simctl list devices available --json | node -e ' + const { readFileSync } = require("node:fs") + const runtime = `com.apple.CoreSimulator.SimRuntime.iOS-${process.env.IOS_SIMULATOR_OS.replaceAll(".", "-")}` + const devices = JSON.parse(readFileSync(0, "utf8")).devices[runtime] ?? [] + const device = devices.find((candidate) => candidate.name === process.env.IOS_SIMULATOR_NAME && candidate.isAvailable) + if (!device) throw new Error(`Simulator ${process.env.IOS_SIMULATOR_NAME} on ${runtime} is unavailable`) + console.log(`${device.udid} ${device.state}`) + ')" + read -r simulator_udid simulator_state <<< "$simulator" + echo "IOS_SIMULATOR_UDID=$simulator_udid" >> "$GITHUB_ENV" + + if [[ "$simulator_state" == "Shutdown" ]]; then + xcrun simctl boot "$simulator_udid" + fi + - name: Restore Pods cache uses: actions/cache@v6 with: @@ -87,8 +106,15 @@ jobs: ONLY_ACTIVE_ARCH=YES \ build \ CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions + + - name: Wait for iOS simulator + timeout-minutes: 10 + run: xcrun simctl bootstatus "$IOS_SIMULATOR_UDID" -b + - name: Run iOS integration tests uses: callstackincubator/react-native-harness@v1.5.0 + env: + HARNESS_DEBUG: "1" with: app: ios/build/DerivedData/Build/Products/Debug-iphonesimulator/NitroSQLiteExample.app runner: ios From c852bfa675294d80819eddd188337e63d03d7755 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 2 Oct 2026 07:38:51 +0200 Subject: [PATCH 3/3] chore: refresh native lockfiles for 10.1.0 --- example/ios/Podfile.lock | 8 ++++---- example/macos/Podfile.lock | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f766b1f8..a04a56c1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1766,7 +1766,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNNitroSQLite (10.0.0): + - RNNitroSQLite (10.1.0): - hermes-engine - NitroModules - RCTRequired @@ -1789,7 +1789,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNNitroSqliteVec (10.0.0): + - RNNitroSqliteVec (10.1.0): - RNNitroSQLite - RNScreens (4.28.0): - hermes-engine @@ -2222,8 +2222,8 @@ SPEC CHECKSUMS: ReactCommon: 749169e12aa3461a3df3656a00e48dbe0456b2d6 ReactNativeDependencies: 94c922c39e0e6150ee9ad05e2ca1946e0ef55783 RNCClipboard: e706ef7ccbbfd69f829c1b683f893bc7668c9fdb - RNNitroSQLite: 6239f0b82c1888b4ced7601da40e9d5c2ee57a91 - RNNitroSqliteVec: 40011b65949eab9ea5d965c0ca3cf2f9f274c072 + RNNitroSQLite: 8f61cb932daf0f55b10310821b9982c43ca5b1c4 + RNNitroSqliteVec: 6f95f4347daf1fcbf64d273170b2fbf235b46a87 RNScreens: 3c977859fd13e67dc4ebfc24afb4d7c2e894db61 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index ad8672bc..0af85bd9 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -2311,7 +2311,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNNitroSQLite (10.0.0): + - RNNitroSQLite (10.1.0): - boost - DoubleConversion - fast_float @@ -2340,7 +2340,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNNitroSqliteVec (10.0.0): + - RNNitroSqliteVec (10.1.0): - RNNitroSQLite - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -2658,8 +2658,8 @@ SPEC CHECKSUMS: ReactCodegen: 4dff081e9dbb8acd8b15a8acc9374b4ac1a9869d ReactCommon: bc273d1cfc32a36ea662e81293e0961de8c21c77 RNCClipboard: 4b58c780f63676367640f23c8e114e9bd0cf86ac - RNNitroSQLite: 1893b4d8d00c1a92acaf838c0816f123fd9e3e2c - RNNitroSqliteVec: 0c255fb910dbcea6c77ab22a9b9b2e10cefdc7f0 + RNNitroSQLite: 2822f30e998d8da8fda8dbe7735d4719f12c27a3 + RNNitroSqliteVec: 040c036e18036398be019353bd26afb1146c6dc4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: a88f30ac6f89fded00f9840e7d043315e5ab4fe0