diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..2f05af981 --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/.flox/cache/service-watcher.state b/.flox/cache/service-watcher.state new file mode 100644 index 000000000..e69de29bb diff --git a/.flox/cache/upgrade-checks.lock b/.flox/cache/upgrade-checks.lock new file mode 100644 index 000000000..e69de29bb diff --git a/.flox/log/upgrade-check.1769209907.log b/.flox/log/upgrade-check.1769209907.log new file mode 100644 index 000000000..41c1aad9b --- /dev/null +++ b/.flox/log/upgrade-check.1769209907.log @@ -0,0 +1,25 @@ +2026-01-23T23:11:47.737456Z DEBUG flox: FLOX_VERSION=1.8.2-g27fa46f +2026-01-23T23:11:47.738740Z DEBUG flox::config: Global config directory overridden path="/Users/abueide/.config/flox" +2026-01-23T23:11:47.738928Z DEBUG flox::utils::init::sentry: No Sentry DSN set, skipping Sentry initialization +2026-01-23T23:11:47.738940Z DEBUG flox: set _FLOX_SUBSYSTEM_VERBOSITY=2 +2026-01-23T23:11:47.739614Z DEBUG flox::config: Global config directory overridden path="/Users/abueide/.config/flox" +2026-01-23T23:11:47.740099Z DEBUG flox::utils::update_notifications: Skipping update check in development mode +2026-01-23T23:11:47.740328Z DEBUG flox::commands: Metrics collection enabled +2026-01-23T23:11:47.740607Z DEBUG flox::utils::init::catalog_client: using catalog client with url: https://api.flox.dev +2026-01-23T23:11:47.742769Z DEBUG flox::commands: feature flags configured=Features { upload: false, qa: false, outputs: false } +2026-01-23T23:11:47.743559Z DEBUG check-upgrade: flox::utils::metrics: pushing entry to metrics buffer: MetricEntry { subcommand: Some("check-upgrade"), extras: {}, timestamp: 2026-01-23 23:11:47.74287 +00:00:00, uuid: 8cd236ce-19ae-492d-a825-647800e32704, flox_version: "1.8.2-g27fa46f", os_family: Some("Mac OS"), os_family_release: Some("24.6.0"), os: None, os_version: None, empty_flags: [] } +2026-01-23T23:11:47.743920Z DEBUG check-upgrade: flox_rust_sdk::models::environment: detected concrete environment type: path +2026-01-23T23:11:47.745018Z DEBUG check-upgrade: flox_rust_sdk::providers::upgrade_checks: created unlocked upgrade information guard upgrade_information_path="/Users/abueide/code/analytics-react-native/.flox/cache/upgrade-checks.json" +2026-01-23T23:11:47.745133Z DEBUG check-upgrade: flox_rust_sdk::providers::upgrade_checks: lock acquired upgrade_information_path="/Users/abueide/code/analytics-react-native/.flox/cache/upgrade-checks.json" +2026-01-23T23:11:47.745179Z DEBUG check-upgrade:check-upgrade: flox_rust_sdk::models::environment::core_environment: upgrading to_upgrade="" +2026-01-23T23:11:47.745358Z DEBUG check-upgrade:check-upgrade: flox_rust_sdk::models::environment::core_environment: checking group membership for requested packages +2026-01-23T23:11:47.745363Z DEBUG check-upgrade:check-upgrade: flox_rust_sdk::models::environment::core_environment: using catalog client to upgrade +2026-01-23T23:11:47.745367Z DEBUG check-upgrade:check-upgrade: flox_rust_sdk::models::environment::core_environment: upgrading to_upgrade="" +2026-01-23T23:11:47.745601Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: composing included environments +2026-01-23T23:11:47.745618Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: inspecting included environment name="common" +2026-01-23T23:11:47.745630Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: found existing locked include for common +2026-01-23T23:11:47.745635Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: using existing locked include from lockfile name="common" +2026-01-23T23:11:47.745640Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: inspecting included environment name="nodejs" +2026-01-23T23:11:47.745644Z DEBUG check-upgrade:check-upgrade:merge_manifest: flox_rust_sdk::models::lockfile: fetching included environment name="nodejs" +2026-01-23T23:11:47.745809Z DEBUG flox::commands: removing process tempdir temp_dir="/Users/abueide/.cache/flox/process/.tmpdm6QIg" +❌ ERROR: failed to fetch environment 'nodejs': Did not find an environment in '/Users/abueide/code/analytics-react-native/github:segment-integrations/templates?dir=env/nodejs' diff --git a/.flox/run/aarch64-darwin.dev.dev b/.flox/run/aarch64-darwin.dev.dev new file mode 120000 index 000000000..9e947cebb --- /dev/null +++ b/.flox/run/aarch64-darwin.dev.dev @@ -0,0 +1 @@ +/nix/store/dfhvq63p6k54ardmc7gwkdm2j03dzg7c-environment-develop \ No newline at end of file diff --git a/.flox/run/aarch64-darwin.dev.run b/.flox/run/aarch64-darwin.dev.run new file mode 120000 index 000000000..1e5793bfd --- /dev/null +++ b/.flox/run/aarch64-darwin.dev.run @@ -0,0 +1 @@ +/nix/store/zr2m4wwpza6pw2y9c9bhiqcnnxl6bckf-environment-runtime \ No newline at end of file diff --git a/.github/workflows/ci-e2e-nightly.yml b/.github/workflows/ci-e2e-nightly.yml new file mode 100644 index 000000000..5385b88b1 --- /dev/null +++ b/.github/workflows/ci-e2e-nightly.yml @@ -0,0 +1,105 @@ +name: E2E (Nightly) + +on: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +concurrency: + group: e2e-nightly-${{ github.ref }} + cancel-in-progress: false + +jobs: + run-e2e-ios: + runs-on: macos-26 + env: + YARN_ENABLE_HARDENED_MODE: 0 + XCODE_VERSION: '26.2' + strategy: + matrix: + include: + - name: ios-min + - name: ios-latest + steps: + - uses: actions/checkout@v4 + - name: Aggressive disk cleanup (macOS) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf /Applications/Android\ Studio.app + sudo rm -rf /usr/local/share/miniconda + sudo rm -rf /opt/homebrew + sudo rm -rf "$HOME/Library/Android" + sudo rm -rf "$HOME/.gradle" + sudo rm -rf "$HOME/Library/Developer/CoreSimulator/Devices" + sudo rm -rf "$HOME/Library/Developer/Xcode/DerivedData" + df -H + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.2' + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'false' + - name: Resolve iOS targets + run: | + . scripts/platform-versions.sh + if [ "${{ matrix.name }}" = "ios-min" ]; then + echo "DETOX_IOS_DEVICE=${PLATFORM_IOS_MIN_DEVICE}" >> "$GITHUB_ENV" + echo "IOS_RUNTIME=${PLATFORM_IOS_MIN_RUNTIME}" >> "$GITHUB_ENV" + else + echo "DETOX_IOS_DEVICE=${PLATFORM_IOS_MAX_DEVICE}" >> "$GITHUB_ENV" + echo "IOS_RUNTIME=${PLATFORM_IOS_MAX_RUNTIME}" >> "$GITHUB_ENV" + fi + - name: iOS E2E Tests + run: devbox shell --omit-nix-env -- devbox run test-ios + + run-e2e-android: + runs-on: ubuntu-24.04-arm + env: + EMU_HEADLESS: 1 + strategy: + matrix: + include: + - name: android-min + target: min + - name: android-latest + target: max + steps: + - uses: actions/checkout@v4 + - name: Aggressive disk cleanup (Ubuntu) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/local/lib/node_modules + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/share/chromium + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /usr/local/share/edge_driver + sudo rm -rf /usr/local/share/gecko_driver + sudo rm -rf /usr/local/share/phantomjs + sudo rm -rf "$HOME/.cache" + df -H + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'false' + - name: Resolve Android targets + run: | + . scripts/platform-versions.sh + if [ "${{ matrix.target }}" = "min" ]; then + api="$PLATFORM_ANDROID_MIN_API" + device="$PLATFORM_ANDROID_MIN_DEVICE" + else + api="$PLATFORM_ANDROID_MAX_API" + device="$PLATFORM_ANDROID_MAX_DEVICE" + fi + avd_name="${device}_API${api}_arm64_v8a" + echo "DETOX_AVD=${avd_name}" >> "$GITHUB_ENV" + - name: Android E2E Tests + run: devbox run test-android diff --git a/.github/workflows/ci-e2e-optional.yml b/.github/workflows/ci-e2e-optional.yml new file mode 100644 index 000000000..5e04ae5cd --- /dev/null +++ b/.github/workflows/ci-e2e-optional.yml @@ -0,0 +1,83 @@ +name: E2E (On-Demand) + +on: + workflow_dispatch: + push: + branches: [ci2] + +concurrency: + group: e2e-optional-${{ github.ref }} + cancel-in-progress: false + +jobs: + run-e2e-ios: + runs-on: macos-26 + env: + YARN_ENABLE_HARDENED_MODE: 0 + XCODE_VERSION: '26.2' + steps: + - uses: actions/checkout@v4 + - name: Aggressive disk cleanup (macOS) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf /Applications/Android\ Studio.app + sudo rm -rf /usr/local/share/miniconda + sudo rm -rf /opt/homebrew + sudo rm -rf "$HOME/Library/Android" + sudo rm -rf "$HOME/.gradle" + sudo rm -rf "$HOME/Library/Developer/CoreSimulator/Devices" + sudo rm -rf "$HOME/Library/Developer/Xcode/DerivedData" + df -H + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.2' + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'false' + - name: Resolve iOS targets + run: | + . scripts/platform-versions.sh + echo "DETOX_IOS_DEVICE=${PLATFORM_IOS_MAX_DEVICE}" >> "$GITHUB_ENV" + echo "IOS_RUNTIME=${PLATFORM_IOS_MAX_RUNTIME}" >> "$GITHUB_ENV" + - name: iOS E2E Tests (latest) + run: devbox shell --omit-nix-env -- devbox run test-ios + + run-e2e-android: + runs-on: ubuntu-24.04-arm + env: + EMU_HEADLESS: 1 + steps: + - uses: actions/checkout@v4 + - name: Aggressive disk cleanup (Ubuntu) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/local/lib/node_modules + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/share/chromium + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /usr/local/share/edge_driver + sudo rm -rf /usr/local/share/gecko_driver + sudo rm -rf /usr/local/share/phantomjs + sudo rm -rf "$HOME/.cache" + df -H + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'false' + - name: Resolve Android targets + run: | + . scripts/platform-versions.sh + api="$PLATFORM_ANDROID_MAX_API" + device="$PLATFORM_ANDROID_MAX_DEVICE" + avd_name="${device}_API${api}_arm64_v8a" + echo "DETOX_AVD=${avd_name}" >> "$GITHUB_ENV" + - name: Android E2E Tests (latest) + run: devbox run test-android diff --git a/.github/workflows/ci-fast.yml b/.github/workflows/ci-fast.yml new file mode 100644 index 000000000..fc37487a8 --- /dev/null +++ b/.github/workflows/ci-fast.yml @@ -0,0 +1,41 @@ +name: CI Fast + +on: + push: + branches: [master, beta] + pull_request: + branches: [master, beta] + +concurrency: + group: ci-fast-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Aggressive disk cleanup (Ubuntu) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/local/lib/node_modules + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/share/chromium + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /usr/local/share/edge_driver + sudo rm -rf /usr/local/share/gecko_driver + sudo rm -rf /usr/local/share/phantomjs + sudo rm -rf "$HOME/.cache" + df -H + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.3.1 + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'false' + - name: build + run: devbox run build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index a83508c95..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,158 +0,0 @@ -name: CI -on: - push: - branches: [master, beta] - pull_request: - branches: [master, beta] - workflow_dispatch: -jobs: - cancel_previous: - runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.12.1 - with: - workflow_id: ${{ github.event.workflow.id }} - build-and-test: - needs: cancel_previous - runs-on: 'ubuntu-latest' - env: - YARN_ENABLE_HARDENED_MODE: 0 - steps: - - uses: actions/checkout@v4 - # Workaround for corepack enable in node - # Source: (https://github.com/actions/setup-node/issues/899#issuecomment-1828798029) - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: corepack enable - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - # End workaround - - - name: Install - run: yarn install --immutable - - name: Build - run: yarn build - - name: Lint - run: yarn lint - - name: Test - run: yarn test --coverage - - run-e2e-ios: - runs-on: 'macos-13' - env: - YARN_ENABLE_HARDENED_MODE: 0 - steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Install applesimutils - run: | - HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew >/dev/null - HOMEBREW_NO_AUTO_UPDATE=1 brew install applesimutils >/dev/null - - - uses: actions/checkout@v4 - # Workaround for corepack enable in node - # Source: (https://github.com/actions/setup-node/issues/899#issuecomment-1828798029) - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: corepack enable - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - # End workaround - - - name: Bootstrap - run: yarn install && yarn e2e install && yarn e2e pods - - - name: Bundle Build - run: yarn build - - - name: Detox - Build - run: RCT_NO_LAUNCH_PACKAGER=1 yarn e2e build:ios - - - name: Detox - Test - run: yarn e2e test:ios - - run-e2e-android: - runs-on: 'macos-13' # This is important, linux cannot run the emulator graphically for e2e tests - strategy: - matrix: - api-level: [21] - profile: ['pixel_xl'] - env: - YARN_ENABLE_HARDENED_MODE: 0 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - - name: Gradle cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} - - - name: AVD cache - uses: actions/cache@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ matrix.api-level }}-${{matrix.profile}} - - - name: create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - profile: ${{matrix.profile}} - avd-name: Pixel_API_21_AOSP - target: default - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching." - - # Workaround for corepack enable in node - # Source: (https://github.com/actions/setup-node/issues/899#issuecomment-1828798029) - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: corepack enable - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - # End workaround - - - name: Bootstrap - run: yarn install && yarn e2e install # No need to run bootstrap here since we don't need cocoapods - - - name: Bundle build - run: yarn build - - - name: Detox - Build - run: RCT_NO_LAUNCH_PACKAGER=1 yarn e2e build:android - - - name: Detox - Test - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - profile: ${{matrix.profile}} - avd-name: Pixel_API_21_AOSP - target: default - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: yarn e2e test:android diff --git a/.github/workflows/create_jira.yml b/.github/workflows/create_jira.yml deleted file mode 100644 index 16e5a4596..000000000 --- a/.github/workflows/create_jira.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Create Jira Ticket - -on: - issues: - types: - - opened - -jobs: - create_jira: - name: Create Jira Ticket - runs-on: ubuntu-latest - environment: IssueTracker - steps: - - name: Checkout - uses: actions/checkout@master - - name: Login - uses: atlassian/gajira-login@master - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_TOKEN }} - - - name: Create - id: create - uses: atlassian/gajira-create@master - with: - project: LIBRARIES - issuetype: Bug - summary: | - [${{ github.event.repository.name }}] (${{ github.event.issue.number }}): ${{ github.event.issue.title }} - description: | - Github Link: ${{ github.event.issue.html_url }} - ${{ github.event.issue.body }} - # Parent and Epic Link fields (set to same) - fields: '{ - "parent": {"key": "LIBRARIES-2048"}, - "customfield_10002": "LIBRARIES-2048" - }' - - - name: Log created issue - run: echo "Issue ${{ steps.create.outputs.issue }} was created" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f19960e67..42e8ef3fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - persist-credentials: false + fetch-depth: 0 token: ${{ secrets.GH_TOKEN }} # Workaround for corepack enable in node @@ -35,18 +35,15 @@ jobs: cache: yarn # End workaround - - name: Config and Build - run: | - npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} - yarn install --immutable - yarn build - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: devbox installer + uses: jetify-com/devbox-install-action@v0.14.0 + with: + enable-cache: 'true' - - name: Publish (All) - if: github.event.inputs.workspace == '' - run: yarn release + - name: Config, Build, Release + run: devbox run release env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }} YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index deb042005..f144e2ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ !.yarn/cache #.pnp.* +.cache/ + # OSX # .DS_Store @@ -91,4 +93,4 @@ tsconfig.tsbuildinfo # Library Info Auto Generated file packages/core/src/info.ts -.pnpm/ \ No newline at end of file +.pnpm/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1df1938ee --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Yarn workspaces live under `packages`: `core` (SDK runtime), `sovran` (state store), `shared` (cross-package utilities), and `plugins/*` (destination and helper plugins). Native iOS/Android sources reside in each package’s `ios` and `android` folders. +- Example apps sit in `examples/AnalyticsReactNativeExample` (manual QA) and `examples/E2E` (Detox). Scripts are in `scripts/`; configuration lives alongside packages (e.g., `tsconfig.json`, `babel.config.js`, `jest.config.js`). + +## Build, Test, and Development Commands +- `yarn bootstrap`: install root + workspace deps and pod install for example/e2e apps. +- `yarn build`: run workspace builds in topo order. +- `yarn testAll` / `yarn test`: workspace Jest suite or root Jest run. +- `yarn lint`; `yarn lint --fix`: ESLint across the monorepo. +- `yarn typescript`: type-check without emitting. +- Example app: `yarn example start | ios | android`. Detox: `yarn e2e start:e2e` then platform builds/tests (e.g., `yarn e2e e2e:build:ios` + `yarn e2e e2e:test:ios`). + +## Coding Style & Naming Conventions +- TypeScript-first; native code should mirror existing Swift/Obj-C/Kotlin style. Two-space indentation and Prettier formatting via ESLint rules. +- Prefer camelCase for variables/functions, PascalCase for React components/classes, and UPPER_SNAKE for constants. Plugin packages follow `plugin-*` folder naming and publish as scoped `@segment/*`. +- Keep public APIs typed and documented; colocate utilities with their feature module (e.g., `src/plugins`, `src/__tests__`). + +## Testing Guidelines +- Unit tests use Jest with tests under `__tests__` near source; snapshots live in `__tests__/__snapshots__`. +- End-to-end coverage uses Detox in `examples/E2E`; build and run per platform before pushing. Add regression tests for new behaviors and keep existing snapshots updated only when intentional. + +## Commit & Pull Request Guidelines +- Commit messages follow Conventional Commits (`feat`, `fix`, `chore`, etc.); enforced by commitlint and release automation. +- For PRs, keep scope narrow, link issues when relevant, and note user-facing changes. Ensure `yarn lint`, `yarn typescript`, and the relevant `yarn test*`/Detox flows pass. Include screenshots only when UI changes affect the example app. + +## Security & Configuration Tips +- Do not commit real Segment write keys or private endpoints; use placeholder values in examples and tests. Keep secrets out of `examples/` and CI config. When testing proxies/CDN settings, prefer environment-driven config rather than hardcoding. diff --git a/devbox.json b/devbox.json new file mode 100644 index 000000000..d95162595 --- /dev/null +++ b/devbox.json @@ -0,0 +1,135 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.2/.schema/devbox.schema.json", + "packages": { + "cocoapods": { + "version": "latest", + "platforms": ["x86_64-darwin", "aarch64-darwin"], + }, + "yarn-berry": "latest", + "treefmt": "latest", + "nixfmt": "latest", + "shfmt": "latest", + "jdk17": "latest", + "gradle": "latest", + "jq": "latest", + "path:./nix#android-sdk": "", + }, + "shell": { + "init_hook": [ + "echo 'Welcome to analytics-react-native devbox!' > /dev/null", + ". $DEVBOX_PROJECT_ROOT/scripts/android-env.sh", + "echo 'Android SDK env configured (details: wiki/devbox.md#devbox-android).'", + ], + "scripts": { + "clean": [ + "rm -rf $DEVBOX_PROJECT_ROOT/examples/E2E/ios/Podfile.lock", + "rm -rf $DEVBOX_PROJECT_ROOT/examples/E2E/ios/Pods", + "cd $DEVBOX_PROJECT_DIR/examples/E2E/android && gradle clean", + "yarn cache clean", + "find $DEVBOX_PROJECT_DIR -type d -name node_modules -exec rmdir {} \\;", + ], + "build": [ + "yarn install --immutable", + "yarn build", + "yarn lint", + "yarn test --coverage", + ], + "test-android": [ + "devbox run setup-android", + "yarn install", + "yarn e2e install", + "yarn build", + "yarn e2e build:android", + "yarn e2e test:android", + ], + "test-ios": [ + "devbox run setup-ios", + "yarn install", + "yarn e2e install", + "yarn e2e pods", + "yarn build", + "yarn e2e build:ios", + "yarn e2e test:ios", + ], + "act-ci": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/act-ci.sh --platform ubuntu-latest=node:20-bullseye" + ], + "setup-android": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/android-setup.sh", + ], + "setup-ios": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/ios-setup.sh", + ], + "start-emulator": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/android-manager.sh start", + ], + "start-ios": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/ios-manager.sh start", + ], + "start-android-minsdk": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/android-manager.sh start", + ], + "start-android-latest": [ + "AVD_FLAVOR=latest bash $DEVBOX_PROJECT_ROOT/scripts/android-manager.sh start", + ], + "start-android": [ + "bash $DEVBOX_PROJECT_ROOT/scripts/android-manager.sh start", + ], + "release": [ + "npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}", + "yarn install --immutable", + "yarn build", + "yarn release", + ], + "reset-android": [ + "rm -rf ~/.android/avd", + "rm -f ~/.android/adbkey*", + "echo \"AVDs and adb keys removed. Recreate via devbox run start-android* as needed.\"", + ], + "reset-ios": [ + "xcrun simctl shutdown all || true", + "xcrun simctl erase all || true", + "xcrun simctl delete all || true", + "xcrun simctl delete unavailable || true", + "killall -9 com.apple.CoreSimulatorService 2>/dev/null || true", + "echo \"Simulators reset via simctl. Recreate via devbox run start-ios.\"", + ], + "stop-android": [ + "if command -v adb >/dev/null 2>&1; then", + " devices=$(adb devices -l 2>/dev/null | tail -n +2 | awk '{print $1}' | tr '\\n' ' ');", + " if [[ -n \"$devices\" ]]; then", + " echo \"Stopping Android emulators: $devices\";", + " for d in $devices; do adb -s \"$d\" emu kill >/dev/null 2>&1 || true; done;", + " else", + " echo \"No Android emulators detected via adb.\";", + " fi;", + "else", + " echo \"adb not found; skipping Android emulator shutdown.\";", + "fi", + "pkill -f \"emulator@\" >/dev/null 2>&1 || true", + "echo \"Android emulators stopped (if any were running).\"", + ], + "stop-ios": [ + "if command -v xcrun >/dev/null 2>&1 && xcrun -f simctl >/dev/null 2>&1; then", + " if xcrun simctl list devices booted | grep -q \"Booted\"; then", + " echo \"Shutting down booted iOS simulators...\";", + " xcrun simctl shutdown all >/dev/null 2>&1 || true;", + " else", + " echo \"No booted iOS simulators detected.\";", + " fi;", + "else", + " echo \"simctl not available; skipping iOS shutdown.\";", + "fi", + "echo \"iOS simulators shutdown (if any were running).\"", + ], + "stop": [ + "devbox run stop-android", + "devbox run stop-ios", + ], + "test": [ + "devbox run test-android", + "devbox run test-ios", + ], + }, + }, +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 000000000..fd248d737 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,278 @@ +{ + "lockfile_version": "1", + "packages": { + "cocoapods@latest": { + "last_modified": "2025-12-31T03:27:36Z", + "resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#cocoapods", + "source": "devbox-search", + "version": "1.16.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/av5g6hfp0yiir3iavg72js70ian8hxyf-cocoapods-1.16.2", + "default": true + } + ], + "store_path": "/nix/store/av5g6hfp0yiir3iavg72js70ian8hxyf-cocoapods-1.16.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/har71589bwmh6h6skisd20b3c6lrwmz7-cocoapods-1.16.2", + "default": true + } + ], + "store_path": "/nix/store/har71589bwmh6h6skisd20b3c6lrwmz7-cocoapods-1.16.2" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-01-20T02:11:35Z", + "resolved": "github:NixOS/nixpkgs/ed142ab1b3a092c4d149245d0c4126a5d7ea00b0?lastModified=1768875095" + }, + "gradle@latest": { + "last_modified": "2025-12-31T03:27:36Z", + "plugin_version": "0.0.1", + "resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#gradle", + "source": "devbox-search", + "version": "8.14.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/v2xbkgrvn0b4g4qq7j5x60va0d4gf0kw-gradle-8.14.3", + "default": true + } + ], + "store_path": "/nix/store/v2xbkgrvn0b4g4qq7j5x60va0d4gf0kw-gradle-8.14.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/mvs8d5c60yyx3sxpppg1r67yjvlrrhhh-gradle-8.14.3", + "default": true + } + ], + "store_path": "/nix/store/mvs8d5c60yyx3sxpppg1r67yjvlrrhhh-gradle-8.14.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jy3fpkvcymjaglzi9z2gbpzcyqypgfxh-gradle-8.14.3", + "default": true + } + ], + "store_path": "/nix/store/jy3fpkvcymjaglzi9z2gbpzcyqypgfxh-gradle-8.14.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/80fgl9ffaxlxl9s4i1jb37krcljx669g-gradle-8.14.3", + "default": true + } + ], + "store_path": "/nix/store/80fgl9ffaxlxl9s4i1jb37krcljx669g-gradle-8.14.3" + } + } + }, + "jdk17@latest": { + "last_modified": "2025-10-22T20:59:19Z", + "resolved": "github:NixOS/nixpkgs/01b6809f7f9d1183a2b3e081f0a1e6f8f415cb09#jdk17", + "source": "devbox-search", + "version": "17.0.12", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/hlm8a8cnp4hm8xkg0a2yy4kv7cq44jii-zulu-ca-jdk-17.0.12", + "default": true + } + ], + "store_path": "/nix/store/hlm8a8cnp4hm8xkg0a2yy4kv7cq44jii-zulu-ca-jdk-17.0.12" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/i4zgq9685y6284hbf5a7ac9ysb88dvlz-zulu-ca-jdk-17.0.12", + "default": true + } + ], + "store_path": "/nix/store/i4zgq9685y6284hbf5a7ac9ysb88dvlz-zulu-ca-jdk-17.0.12" + } + } + }, + "jq@latest": { + "last_modified": "2026-01-12T00:44:08Z", + "resolved": "github:NixOS/nixpkgs/3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0#jq", + "source": "devbox-search", + "version": "1.8.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/9rm6fm3zq1jq8rgsx528cw8wkmfya2gf-jq-1.8.1-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/cv999saj62xhq7xv5i7q6944vljykfmw-jq-1.8.1-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/5camppj4hz2mgkdbxs0kr6nvh6qa65wf-jq-1.8.1-dev" + }, + { + "name": "doc", + "path": "/nix/store/lak094rhhxlaj1qycadmxyfphgjadj5r-jq-1.8.1-doc" + }, + { + "name": "out", + "path": "/nix/store/g371yvjasdr552v98p5kav7n35s1dfib-jq-1.8.1" + } + ], + "store_path": "/nix/store/9rm6fm3zq1jq8rgsx528cw8wkmfya2gf-jq-1.8.1-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/m8qv4g54q3jmjb8i33v9lljcwhydx2vd-jq-1.8.1-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/9x2457g76jikfy7xq4mjqwzl8iz3zvxj-jq-1.8.1-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/5ykn83b3hhvnnq0p5vqgcrzihrl9wpsl-jq-1.8.1-dev" + }, + { + "name": "doc", + "path": "/nix/store/37ypy1595g6rj3cymh1mpk2b25fx40g7-jq-1.8.1-doc" + }, + { + "name": "out", + "path": "/nix/store/16lg603jzppwjanlakcak1ais69mkd03-jq-1.8.1" + } + ], + "store_path": "/nix/store/m8qv4g54q3jmjb8i33v9lljcwhydx2vd-jq-1.8.1-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/kkb17whpkdrmn9g3gk7y6l69vipxsw0i-jq-1.8.1-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/iwr61wi83kflqvz8j5nf7ridaqq6nh2w-jq-1.8.1-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/lypnqs272644l8ff6wfji9rg5jw10v7h-jq-1.8.1-dev" + }, + { + "name": "doc", + "path": "/nix/store/nyw97c4pywfcqqap5hyk9xjghczlbshl-jq-1.8.1-doc" + }, + { + "name": "out", + "path": "/nix/store/ri930a557685c64bdh88a5031i7hx3vy-jq-1.8.1" + } + ], + "store_path": "/nix/store/kkb17whpkdrmn9g3gk7y6l69vipxsw0i-jq-1.8.1-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/zssasryipb2x4gk2ahzacl4mvvcmk48j-jq-1.8.1-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/7d4pv1iymyqk2lykwj1ydml3rjhc6gl3-jq-1.8.1-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/rmxxm5jnxq93kvkhbr2b3hzj6v3ldp8z-jq-1.8.1-dev" + }, + { + "name": "doc", + "path": "/nix/store/mkhfvc69grlky3iblibkw9wcc12jcdqq-jq-1.8.1-doc" + }, + { + "name": "out", + "path": "/nix/store/807g765zgpmp1c8fm5y40rw2gbr1k6dk-jq-1.8.1" + } + ], + "store_path": "/nix/store/zssasryipb2x4gk2ahzacl4mvvcmk48j-jq-1.8.1-bin" + } + } + }, + "yarn-berry@latest": { + "last_modified": "2025-12-31T03:27:36Z", + "resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#yarn-berry", + "source": "devbox-search", + "version": "4.12.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2l7sbyyqardvrzr35zkrw67gbng5gb8y-yarn-berry-4.12.0", + "default": true + } + ], + "store_path": "/nix/store/2l7sbyyqardvrzr35zkrw67gbng5gb8y-yarn-berry-4.12.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/klx9ndw1djgx0zhhyrkcn9an094rmmwv-yarn-berry-4.12.0", + "default": true + } + ], + "store_path": "/nix/store/klx9ndw1djgx0zhhyrkcn9an094rmmwv-yarn-berry-4.12.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/m6cwiya6hrbwnlprh2cbnmz6c7mkylrf-yarn-berry-4.12.0", + "default": true + } + ], + "store_path": "/nix/store/m6cwiya6hrbwnlprh2cbnmz6c7mkylrf-yarn-berry-4.12.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/q1gys3zgijcciiafbh9nfawkx5wj8179-yarn-berry-4.12.0", + "default": true + } + ], + "store_path": "/nix/store/q1gys3zgijcciiafbh9nfawkx5wj8179-yarn-berry-4.12.0" + } + } + } + } +} diff --git a/examples/E2E/.detoxrc.js b/examples/E2E/.detoxrc.js index 98759a8cb..3013348a6 100644 --- a/examples/E2E/.detoxrc.js +++ b/examples/E2E/.detoxrc.js @@ -1,3 +1,81 @@ +const { execSync } = require('child_process'); + +const defaultIOSDeviceCandidates = (() => { + const fromEnv = (process.env.IOS_DEVICE_NAMES || 'iPhone 14') + .split(',') + .map((name) => name.trim()) + .filter(Boolean); + return Array.from(new Set(['iPhone 17', ...fromEnv, 'iPhone 14'])); +})(); + +const safeParseJSON = (cmd) => { + try { + return JSON.parse( + execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString() + ); + } catch (_) { + return null; + } +}; + +const listAvailableDevices = () => { + const parsed = safeParseJSON('xcrun simctl list devices -j'); + if (!parsed || !parsed.devices) return []; + const devices = []; + Object.values(parsed.devices).forEach((group) => { + (group || []).forEach((device) => { + if (device.isAvailable || device.availability === '(available)') { + devices.push(device); + } + }); + }); + return devices; +}; + +const listAvailableDeviceTypes = () => { + const parsed = safeParseJSON('xcrun simctl list devicetypes -j'); + if (!parsed || !parsed.devicetypes) return new Set(); + return new Set(parsed.devicetypes.map((dt) => dt.name.toLowerCase())); +}; + +const baseName = (name) => name.split(' (')[0].trim().toLowerCase(); + +const detectIOSDevice = () => { + if (process.env.DETOX_IOS_DEVICE) { + return { type: process.env.DETOX_IOS_DEVICE }; + } + + const devices = listAvailableDevices(); + const preferredDevice = defaultIOSDeviceCandidates.find((candidate) => + devices.some((d) => baseName(d.name) === candidate.toLowerCase()) + ); + if (preferredDevice) { + const found = devices.find( + (d) => baseName(d.name) === preferredDevice.toLowerCase() + ); + if (found) return { name: found.name }; + } + const anyIphoneDevice = devices.find((d) => + d.name.toLowerCase().includes('iphone') + ); + if (anyIphoneDevice) return { name: anyIphoneDevice.name }; + + const availableTypes = listAvailableDeviceTypes(); + const preferredType = defaultIOSDeviceCandidates.find((candidate) => + availableTypes.has(candidate.toLowerCase()) + ); + if (preferredType) return { type: preferredType }; + + const anyIphoneType = Array.from(availableTypes).find((t) => + t.includes('iphone') + ); + if (anyIphoneType) return { type: anyIphoneType }; + + return { type: defaultIOSDeviceCandidates[0] }; +}; + +const iosDevice = detectIOSDevice(); + /** @type {Detox.DetoxConfig} */ module.exports = { testRunner: { @@ -51,7 +129,8 @@ module.exports = { simulator: { type: 'ios.simulator', device: { - type: 'iPhone 14' + // Allow CI/local override; defaults to an available simulator by name (or type fallback). + ...iosDevice } }, attached: { @@ -63,7 +142,12 @@ module.exports = { emulator: { type: 'android.emulator', device: { - avdName: process.env.CI ? 'Pixel_API_21_AOSP': 'Pixel_3a_API_32' + // Default to latest AVD name (arch-aware); override via DETOX_AVD. For minsdk testing, set DETOX_AVD to an API 21 AVD. + avdName: (() => { + if (process.env.DETOX_AVD) return process.env.DETOX_AVD; + const arch = require('os').arch(); + return arch === 'arm64' ? 'medium_phone_API33_arm64_v8a' : 'medium_phone_API33_x86_64'; + })() } } }, diff --git a/examples/E2E/android/app/build.gradle b/examples/E2E/android/app/build.gradle index bc8fe887d..2afd7a5c5 100644 --- a/examples/E2E/android/app/build.gradle +++ b/examples/E2E/android/app/build.gradle @@ -112,12 +112,6 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") - debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { - exclude group:'com.squareup.okhttp3', module:'okhttp' - } - - debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { diff --git a/examples/E2E/android/app/src/main/java/com/analyticsreactnativeexample/MainApplication.java b/examples/E2E/android/app/src/main/java/com/analyticsreactnativeexample/MainApplication.java index cd5b079b6..b7ee722d5 100644 --- a/examples/E2E/android/app/src/main/java/com/analyticsreactnativeexample/MainApplication.java +++ b/examples/E2E/android/app/src/main/java/com/analyticsreactnativeexample/MainApplication.java @@ -57,6 +57,5 @@ public void onCreate() { // If you opted-in for the New Architecture, we load the native entry point for this app. DefaultNewArchitectureEntryPoint.load(); } - ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); } } diff --git a/examples/E2E/android/build.gradle b/examples/E2E/android/build.gradle index 9c2d29f43..c83bf5b8a 100644 --- a/examples/E2E/android/build.gradle +++ b/examples/E2E/android/build.gradle @@ -2,7 +2,9 @@ buildscript { ext { - buildToolsVersion = "33.0.0" + // Default to the build-tools pinned in devbox; allow override via ANDROID_BUILD_TOOLS_VERSION. + // Keep in sync with nix/flake.nix when ANDROID_BUILD_TOOLS_VERSION is unset. + buildToolsVersion = System.getenv("ANDROID_BUILD_TOOLS_VERSION") ?: "30.0.3" minSdkVersion = 21 compileSdkVersion = 33 targetSdkVersion = 33 @@ -34,4 +36,4 @@ allprojects { jcenter() maven { url 'https://www.jitpack.io' } } -} \ No newline at end of file +} diff --git a/examples/E2E/android/gradle.properties b/examples/E2E/android/gradle.properties index a3b2fa124..a46a5b90f 100644 --- a/examples/E2E/android/gradle.properties +++ b/examples/E2E/android/gradle.properties @@ -24,9 +24,6 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Version of flipper SDK to use with React Native -FLIPPER_VERSION=0.182.0 - # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 diff --git a/examples/E2E/ios/Podfile b/examples/E2E/ios/Podfile index 0e91586a8..e8c21a1c7 100644 --- a/examples/E2E/ios/Podfile +++ b/examples/E2E/ios/Podfile @@ -8,17 +8,6 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! -# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. -# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded -# -# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` -# ```js -# module.exports = { -# dependencies: { -# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), -# ``` -flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled - linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green @@ -36,11 +25,6 @@ target 'AnalyticsReactNativeE2E' do # Hermes is now enabled by default. Disable by setting this flag to false. :hermes_enabled => flags[:hermes_enabled], :fabric_enabled => flags[:fabric_enabled], - # Enables Flipper. - # - # Note that if you have use_frameworks! enabled, Flipper will not work and - # you should disable the next line. - :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) @@ -51,6 +35,11 @@ target 'AnalyticsReactNativeE2E' do end post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.4' + end + end # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, diff --git a/examples/E2E/ios/Podfile.lock b/examples/E2E/ios/Podfile.lock index 3b2b01152..0039daf46 100644 --- a/examples/E2E/ios/Podfile.lock +++ b/examples/E2E/ios/Podfile.lock @@ -1,6 +1,5 @@ PODS: - boost (1.76.0) - - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - FBLazyVector (0.72.9) - FBReactNativeSpec (0.72.9): @@ -10,71 +9,12 @@ PODS: - React-Core (= 0.72.9) - React-jsi (= 0.72.9) - ReactCommon/turbomodule/core (= 0.72.9) - - Flipper (0.182.0): - - Flipper-Folly (~> 2.6) - - Flipper-Boost-iOSX (1.76.0.1.11) - - Flipper-DoubleConversion (3.2.0.1) - - Flipper-Fmt (7.1.7) - - Flipper-Folly (2.6.10): - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt (= 7.1.7) - - Flipper-Glog - - libevent (~> 2.1.12) - - OpenSSL-Universal (= 1.1.1100) - - Flipper-Glog (0.5.0.5) - - Flipper-PeerTalk (0.0.4) - - FlipperKit (0.182.0): - - FlipperKit/Core (= 0.182.0) - - FlipperKit/Core (0.182.0): - - Flipper (~> 0.182.0) - - FlipperKit/CppBridge - - FlipperKit/FBCxxFollyDynamicConvert - - FlipperKit/FBDefines - - FlipperKit/FKPortForwarding - - SocketRocket (~> 0.6.0) - - FlipperKit/CppBridge (0.182.0): - - Flipper (~> 0.182.0) - - FlipperKit/FBCxxFollyDynamicConvert (0.182.0): - - Flipper-Folly (~> 2.6) - - FlipperKit/FBDefines (0.182.0) - - FlipperKit/FKPortForwarding (0.182.0): - - CocoaAsyncSocket (~> 7.6) - - Flipper-PeerTalk (~> 0.0.4) - - FlipperKit/FlipperKitHighlightOverlay (0.182.0) - - FlipperKit/FlipperKitLayoutHelpers (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutTextSearchable - - FlipperKit/FlipperKitLayoutIOSDescriptors (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - YogaKit (~> 1.18) - - FlipperKit/FlipperKitLayoutPlugin (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - FlipperKit/FlipperKitLayoutIOSDescriptors - - FlipperKit/FlipperKitLayoutTextSearchable - - YogaKit (~> 1.18) - - FlipperKit/FlipperKitLayoutTextSearchable (0.182.0) - - FlipperKit/FlipperKitNetworkPlugin (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitReactPlugin (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitUserDefaultsPlugin (0.182.0): - - FlipperKit/Core - - FlipperKit/SKIOSNetworkPlugin (0.182.0): - - FlipperKit/Core - - FlipperKit/FlipperKitNetworkPlugin - fmt (6.2.1) - glog (0.3.5) - hermes-engine (0.72.9): - hermes-engine/Pre-built (= 0.72.9) - hermes-engine/Pre-built (0.72.9) - libevent (2.1.12) - - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -499,45 +439,22 @@ PODS: - RNScreens (3.27.0): - RCT-Folly (= 2021.07.22.00) - React-Core - - segment-analytics-react-native (2.20.2): + - segment-analytics-react-native (2.21.4): - React-Core - sovran-react-native - SocketRocket (0.6.1) - - sovran-react-native (1.1.2): + - sovran-react-native (1.1.3): - React-Core - Yoga (1.14.0) - - YogaKit (1.18.1): - - Yoga (~> 1.14) DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - Flipper (= 0.182.0) - - Flipper-Boost-iOSX (= 1.76.0.1.11) - - Flipper-DoubleConversion (= 3.2.0.1) - - Flipper-Fmt (= 7.1.7) - - Flipper-Folly (= 2.6.10) - - Flipper-Glog (= 0.5.0.5) - - Flipper-PeerTalk (= 0.0.4) - - FlipperKit (= 0.182.0) - - FlipperKit/Core (= 0.182.0) - - FlipperKit/CppBridge (= 0.182.0) - - FlipperKit/FBCxxFollyDynamicConvert (= 0.182.0) - - FlipperKit/FBDefines (= 0.182.0) - - FlipperKit/FKPortForwarding (= 0.182.0) - - FlipperKit/FlipperKitHighlightOverlay (= 0.182.0) - - FlipperKit/FlipperKitLayoutPlugin (= 0.182.0) - - FlipperKit/FlipperKitLayoutTextSearchable (= 0.182.0) - - FlipperKit/FlipperKitNetworkPlugin (= 0.182.0) - - FlipperKit/FlipperKitReactPlugin (= 0.182.0) - - FlipperKit/FlipperKitUserDefaultsPlugin (= 0.182.0) - - FlipperKit/SKIOSNetworkPlugin (= 0.182.0) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) - - OpenSSL-Universal (= 1.1.1100) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) @@ -545,7 +462,6 @@ DEPENDENCIES: - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - React-Codegen (from `build/generated/ios`) - React-Core (from `../node_modules/react-native/`) - - React-Core/DevSupport (from `../node_modules/react-native/`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) @@ -584,20 +500,9 @@ DEPENDENCIES: SPEC REPOS: trunk: - - CocoaAsyncSocket - - Flipper - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt - - Flipper-Folly - - Flipper-Glog - - Flipper-PeerTalk - - FlipperKit - fmt - libevent - - OpenSSL-Universal - SocketRocket - - YogaKit EXTERNAL SOURCES: boost: @@ -697,23 +602,13 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7dcd2de282d72e344012f7d6564d024930a6a440 - CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: dc178b8748748c036ef9493a5d59d6d1f91a36ce FBReactNativeSpec: d0aaae78e93c89dc2d691d8052a4d2aeb1b461ee - Flipper: 6edb735e6c3e332975d1b17956bcc584eccf5818 - Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c - Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 - Flipper-Fmt: 60cbdd92fc254826e61d669a5d87ef7015396a9b - Flipper-Folly: 584845625005ff068a6ebf41f857f468decd26b3 - Flipper-Glog: 70c50ce58ddaf67dc35180db05f191692570f446 - Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 - FlipperKit: 2efad7007d6745a3f95e4034d547be637f89d3f6 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 9b9bb14184a11b8ceb4131b09abf634880f0f46d libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: f30c3213569b1dc43659ecc549a6536e1e11139e RCTTypeSafety: e1ed3137728804fa98bce30b70e3da0b8e23054e @@ -752,12 +647,11 @@ SPEC CHECKSUMS: RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581 - segment-analytics-react-native: 53785e35d44a0643beffa40eada68a4cbdf7292e + segment-analytics-react-native: 05c3bf2adb8a3be2c273808a6fdaced06d927917 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - sovran-react-native: 5f02bd2d111ffe226d00c7b0435290eae6f10934 + sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60 Yoga: eddf2bbe4a896454c248a8f23b4355891eb720a6 - YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 9d352ca8db1e31a063d2585ed47fdadabf87fe90 +PODFILE CHECKSUM: a4c187e503408b85ffe8c89a4cb726ec541057ce -COCOAPODS: 1.11.3 +COCOAPODS: 1.16.2 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..a468eda9e --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.home=/nix/store/hlm8a8cnp4hm8xkg0a2yy4kv7cq44jii-zulu-ca-jdk-17.0.12 diff --git a/multi-release.config.js b/multi-release.config.js index 4a5fc7895..d7dccc284 100644 --- a/multi-release.config.js +++ b/multi-release.config.js @@ -1,7 +1,11 @@ module.exports = { - tagFormat: "${name}-v${version}", + branches: ['master', { name: 'beta', prerelease: true }], + tagFormat: '${name}-v${version}', deps: { bump: 'satisfy', // Do not trigger a release for every package if the only change is a minor/patch upgrade of dependencies - prefix: '^' // by default all semvers will get set to ^major version - } -} \ No newline at end of file + prefix: '^', // by default all semvers will get set to ^major version + }, + ignorePrivate: true, + sequentialInit: true, + sequentialPrepare: true, +}; diff --git a/nix/flake.lock b/nix/flake.lock new file mode 100644 index 000000000..9bfc9cd96 --- /dev/null +++ b/nix/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1768364046, + "narHash": "sha256-PDFfpswLiuG/DcadTBb7dEfO3jX1fcGlCD4ZKSkC0M8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ea30586ee015f37f38783006a9bc9e4aa64d7d61", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/flake.nix b/nix/flake.nix new file mode 100644 index 000000000..b359730d0 --- /dev/null +++ b/nix/flake.nix @@ -0,0 +1,75 @@ +{ + description = "Slim Android SDK tools for Devbox via flakes"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + versionsFile = builtins.readFile ./scripts/platform-versions.sh; + versionLines = builtins.splitString "\n" versionsFile; + getVar = name: default: + let + line = builtins.findFirst (l: builtins.match ("^" + name + "=") l != null) "" versionLines; + raw = if line == "" then default else builtins.elemAt (builtins.splitString "=" line) 1; + cleaned = builtins.replaceStrings ["\"" "'"] [ "" "" ] raw; + in cleaned; + + androidSdkConfig = { + platformVersions = [ + (getVar "PLATFORM_ANDROID_MIN_API" "21") + (getVar "PLATFORM_ANDROID_MAX_API" "33") + ]; + buildToolsVersion = getVar "PLATFORM_ANDROID_BUILD_TOOLS_VERSION" "30.0.3"; + cmdLineToolsVersion = getVar "PLATFORM_ANDROID_CMDLINE_TOOLS_VERSION" "19.0"; + systemImageTypes = [ (getVar "PLATFORM_ANDROID_SYSTEM_IMAGE_TAG" "google_apis") ]; + }; + + forAllSystems = f: + builtins.listToAttrs (map (system: { + name = system; + value = f system; + }) systems); + in + { + packages = forAllSystems (system: + let + pkgs = import nixpkgs { + inherit system; + config = { + allowUnfree = true; + android_sdk.accept_license = true; + }; + }; + + abiVersions = + if builtins.match "aarch64-.*" system != null + then [ "arm64-v8a" ] + else [ "x86_64" ]; + + androidPkgs = pkgs.androidenv.composeAndroidPackages { + # Keep API 21 images for the AVD and add API 33 for React Native builds. + platformVersions = androidSdkConfig.platformVersions; + buildToolsVersions = [ androidSdkConfig.buildToolsVersion ]; + cmdLineToolsVersion = androidSdkConfig.cmdLineToolsVersion; + includeEmulator = true; + includeSystemImages = true; + includeNDK = false; + abiVersions = abiVersions; + systemImageTypes = androidSdkConfig.systemImageTypes; + }; + in + { + android-sdk = androidPkgs.androidsdk; + default = androidPkgs.androidsdk; + }); + + androidSdkConfig = androidSdkConfig; + }; +} diff --git a/package.json b/package.json index 5836c4baf..d6232414c 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,15 @@ "sovran": "yarn workspace @segment/sovran-react-native", "example": "yarn --cwd examples/AnalyticsReactNativeExample", "e2e": "yarn --cwd examples/E2E", - "build": "yarn workspaces foreach -A --topological-dev run build", + "build": "yarn workspaces foreach -A -p --topological-dev run build", "testAll": "yarn workspaces foreach -A -p run test --passWithNoTests", "clean": "yarn workspaces foreach -A -p run clean", "typescript": "tsc --noEmit --composite false", "test": "jest", "lint": "eslint .", - "release": " yarn multi-semantic-release" + "format": "treefmt --clear-cache", + "format:check": "treefmt --clear-cache --fail-on-change", + "release": "yarn multi-semantic-release" }, "devDependencies": { "@anolilab/multi-semantic-release": "^1.0.3", diff --git a/release.config.js b/release.config.js index 657d2e34b..cfef80cd1 100644 --- a/release.config.js +++ b/release.config.js @@ -1,13 +1,18 @@ +const changelogFile = 'CHANGELOG.md'; + module.exports = { + branches: [ + 'master', + { name: 'beta', prerelease: true }, + ], + tagFormat: '${name}-v${version}', plugins: [ - [ - '@semantic-release/commit-analyzer', - { preset: 'conventionalcommits' } - ], - '@semantic-release/changelog', - 'semantic-release-yarn', - '@semantic-release/github', - '@semantic-release/git' + ['@semantic-release/commit-analyzer', { preset: 'conventionalcommits' }], + ['@semantic-release/release-notes-generator', { preset: 'conventionalcommits' }], + ['@semantic-release/changelog', { changelogFile }], + ['@semantic-release/npm', { npmPublish: true }], + ['@semantic-release/github', { successComment: false }], + ['@semantic-release/git', { assets: [changelogFile, 'package.json'] }], ], - debug: true + debug: true, }; diff --git a/scripts/act-ci.sh b/scripts/act-ci.sh new file mode 100755 index 000000000..71ae350f7 --- /dev/null +++ b/scripts/act-ci.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run GitHub Actions workflows locally via act. +# Usage: scripts/act-ci.sh [--job JOB] [--platform ubuntu-latest=node:20-bullseye] + +JOB="" +PLATFORM="ubuntu-latest=node:20-bullseye" + +while [[ $# -gt 0 ]]; do + case "$1" in + -j|--job) + JOB="$2"; shift 2 ;; + -p|--platform) + PLATFORM="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +CMD=(act) +CMD+=(--pull=false) +CMD+=(--platform "${PLATFORM}") +CMD+=(--input ACT=true) +if [[ -n "$JOB" ]]; then + CMD+=(--job "$JOB") +fi + +printf 'Running: %s\n' "${CMD[*]}" +exec "${CMD[@]}" diff --git a/scripts/android-env.sh b/scripts/android-env.sh new file mode 100755 index 000000000..a6d11fa68 --- /dev/null +++ b/scripts/android-env.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env sh +# Sets ANDROID_SDK_ROOT/ANDROID_HOME and PATH to the flake-pinned SDK if not already set. + +# Load shared platform versions if present. +script_dir="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$script_dir/platform-versions.sh" ]; then + # shellcheck disable=SC1090 + . "$script_dir/platform-versions.sh" +fi + +if [ -z "${ANDROID_MIN_API:-}" ] && [ -n "${PLATFORM_ANDROID_MIN_API:-}" ]; then + ANDROID_MIN_API="$PLATFORM_ANDROID_MIN_API" +fi +if [ -z "${ANDROID_MAX_API:-}" ] && [ -n "${PLATFORM_ANDROID_MAX_API:-}" ]; then + ANDROID_MAX_API="$PLATFORM_ANDROID_MAX_API" +fi +if [ -z "${ANDROID_BUILD_TOOLS_VERSION:-}" ] && [ -n "${PLATFORM_ANDROID_BUILD_TOOLS_VERSION:-}" ]; then + ANDROID_BUILD_TOOLS_VERSION="$PLATFORM_ANDROID_BUILD_TOOLS_VERSION" +fi +if [ -z "${ANDROID_CMDLINE_TOOLS_VERSION:-}" ] && [ -n "${PLATFORM_ANDROID_CMDLINE_TOOLS_VERSION:-}" ]; then + ANDROID_CMDLINE_TOOLS_VERSION="$PLATFORM_ANDROID_CMDLINE_TOOLS_VERSION" +fi +if [ -z "${ANDROID_SYSTEM_IMAGE_TAG:-}" ] && [ -n "${PLATFORM_ANDROID_SYSTEM_IMAGE_TAG:-}" ]; then + ANDROID_SYSTEM_IMAGE_TAG="$PLATFORM_ANDROID_SYSTEM_IMAGE_TAG" +fi + +# Only act if neither var is already provided. +if [ -z "${ANDROID_SDK_ROOT:-}" ] && [ -z "${ANDROID_HOME:-}" ]; then + DEVBOX_SDK_OUT=$( + nix --extra-experimental-features 'nix-command flakes' \ + eval --raw "path:${DEVBOX_PROJECT_ROOT}/nix#android-sdk.outPath" 2>/dev/null || true + ) + if [ -n "${DEVBOX_SDK_OUT:-}" ] && [ -d "$DEVBOX_SDK_OUT/libexec/android-sdk" ]; then + ANDROID_SDK_ROOT="$DEVBOX_SDK_OUT/libexec/android-sdk" + ANDROID_HOME="$ANDROID_SDK_ROOT" + fi +fi + +if [ -z "${ANDROID_SDK_ROOT:-}" ] && [ -n "${ANDROID_HOME:-}" ]; then + ANDROID_SDK_ROOT="$ANDROID_HOME" +fi + +if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -z "${ANDROID_HOME:-}" ]; then + ANDROID_HOME="$ANDROID_SDK_ROOT" +fi + +export ANDROID_SDK_ROOT ANDROID_HOME +export ANDROID_BUILD_TOOLS_VERSION + +if [ -n "${ANDROID_SDK_ROOT:-}" ]; then + # Prefer cmdline-tools;latest, or fall back to the highest numbered cmdline-tools folder. + cmdline_tools_bin="" + if [ -d "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin" ]; then + cmdline_tools_bin="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin" + else + cmdline_tools_dir=$(find "$ANDROID_SDK_ROOT/cmdline-tools" -maxdepth 1 -mindepth 1 -type d -not -name latest 2>/dev/null | sort -V | tail -n 1) + if [ -n "${cmdline_tools_dir:-}" ] && [ -d "$cmdline_tools_dir/bin" ]; then + cmdline_tools_bin="$cmdline_tools_dir/bin" + fi + fi + + new_path="$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/platform-tools" + + if [ -n "${cmdline_tools_bin:-}" ]; then + new_path="$new_path:$cmdline_tools_bin" + fi + + new_path="$new_path:$ANDROID_SDK_ROOT/tools/bin:$PATH" + PATH="$new_path" + export PATH + echo "Using Android SDK: $ANDROID_SDK_ROOT" + case "$ANDROID_SDK_ROOT" in + /nix/store/*) + echo "Source: Nix flake (reproducible, pinned). To use your local SDK instead, set ANDROID_HOME/ANDROID_SDK_ROOT before starting devbox shell." + ;; + *) + echo "Source: User/local SDK. To use the pinned Nix SDK, unset ANDROID_HOME/ANDROID_SDK_ROOT before starting devbox shell." + ;; + esac +else + echo "Android SDK not set; using system PATH" +fi diff --git a/scripts/android-manager.sh b/scripts/android-manager.sh new file mode 100755 index 000000000..776d16a06 --- /dev/null +++ b/scripts/android-manager.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +action="${1:-}"; shift || true + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/android-env.sh" + +start_android() { + local flavor="${AVD_FLAVOR:-minsdk}" headless="${EMU_HEADLESS:-}" port="${EMU_PORT:-5554}" + local avd="${DETOX_AVD:-}" + + if [[ -z "$avd" ]]; then + if [[ "$flavor" == "latest" ]]; then + local host_arch + host_arch="$(uname -m)" + avd="medium_phone_API33_$( [[ "$host_arch" == "arm64" || "$host_arch" == "aarch64" ]] && echo arm64_v8a || echo x86_64 )" + else + avd="pixel_API21_$(uname -m | grep -qi arm && echo arm64_v8a || echo x86_64)" + fi + fi + + devbox run setup-android + local target_serial="emulator-${port}" + if command -v adb >/dev/null 2>&1; then + adb devices | awk 'NR>1 && $2=="offline" {print $1}' | while read -r d; do adb -s "$d" emu kill >/dev/null 2>&1 || true; done + fi + echo "Starting Android emulator: ${avd} (flavor ${flavor}, port ${port}, headless=${headless:-0})" + emulator -avd "${avd}" ${headless:+-no-window} -port "${port}" -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -accel on -writable-system -no-snapshot-save & + adb -s "${target_serial}" wait-for-device + local boot_completed="" + until [ "$boot_completed" = "1" ]; do + boot_completed=$(adb -s "${target_serial}" shell getprop sys.boot_completed 2>/dev/null | tr -d "\r") + sleep 5 + done + adb -s "${target_serial}" shell settings put global window_animation_scale 0 + adb -s "${target_serial}" shell settings put global transition_animation_scale 0 + adb -s "${target_serial}" shell settings put global animator_duration_scale 0 +} + +stop_android() { + devbox run stop-android +} + +reset_android() { + devbox run reset-android +} + +case "$action" in + start) start_android ;; + stop) stop_android ;; + reset) reset_android ;; + *) echo "Usage: android-manager.sh {start|stop|reset}" >&2; exit 1 ;; +esac diff --git a/scripts/android-setup.sh b/scripts/android-setup.sh new file mode 100755 index 000000000..0dc005ebe --- /dev/null +++ b/scripts/android-setup.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates AVDs using the Android SDK provided by devbox/flake (system images, emulator, NDK already installed). +# Run inside a devbox shell so SDK tools are available. +# Configurable via env: +# AVD_API (default 21) +# AVD_DEVICE (default "pixel") +# AVD_TAG (default "google_apis") +# AVD_ABI (preferred ABI; optional) +# AVD_NAME (override final AVD name; otherwise computed) +# Secondary AVD (created in addition to the primary): +# AVD_SECONDARY_API (default 33) +# AVD_SECONDARY_DEVICE (default "medium_phone") +# AVD_SECONDARY_TAG (default "google_apis") +# AVD_SECONDARY_ABI (preferred ABI; optional) +# AVD_SECONDARY_NAME (override final name) + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required tool: $1. Ensure devbox shell is active and required packages are installed." >&2 + exit 1 + fi +} + +script_dir="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$script_dir/platform-versions.sh" ]; then + # shellcheck disable=SC1090 + . "$script_dir/platform-versions.sh" +fi + +detect_sdk_root() { + if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then + echo "$ANDROID_SDK_ROOT" + return + fi + + local sm + sm="$(command -v sdkmanager 2>/dev/null || true)" + if [[ -z "$sm" ]]; then + return + fi + sm="$(readlink -f "$sm")" + local candidates=( + "$(dirname "$sm")/.." + "$(dirname "$sm")/../share/android-sdk" + "$(dirname "$sm")/../libexec/android-sdk" + "$(dirname "$sm")/../.." + ) + for c in "${candidates[@]}"; do + if [[ -d "$c/platform-tools" || -d "$c/platforms" || -d "$c/system-images" ]]; then + echo "$c" + return + fi + done +} + +avd_exists() { + local name="$1" + avdmanager list avd | grep -q "Name: ${name}" +} + +pick_image() { + local api="$1" tag="$2" preferred_abi="$3" + local host_arch + host_arch="$(uname -m)" + + local candidates=() + if [[ -n "${preferred_abi:-}" ]]; then + candidates=("$preferred_abi") + else + case "$host_arch" in + arm64|aarch64) candidates=("arm64-v8a" "x86_64" "x86") ;; + *) candidates=("x86_64" "x86" "arm64-v8a") ;; + esac + fi + + for abi in "${candidates[@]}"; do + local image="system-images;android-${api};${tag};${abi}" + local path="${ANDROID_SDK_ROOT}/system-images/android-${api}/${tag}/${abi}" + if [[ -d "$path" ]]; then + echo "$image" + return 0 + fi + done + + return 1 +} + +create_avd() { + local name="$1" device="$2" image="$3" + local abi="${image##*;}" + + if avd_exists "$name"; then + echo "AVD ${name} already exists." + return 0 + fi + + echo "Creating AVD ${name} with ${image}..." + avdmanager create avd --force --name "$name" --package "$image" --device "$device" --abi "$abi" --sdcard 512M +} + +main() { + local detected_sdk_root + detected_sdk_root="$(detect_sdk_root)" + + if [[ -z "${ANDROID_SDK_ROOT:-}" && -n "$detected_sdk_root" ]]; then + export ANDROID_SDK_ROOT="$detected_sdk_root" + fi + + if [[ -z "${ANDROID_SDK_ROOT:-}" && -z "${ANDROID_HOME:-}" ]]; then + echo "ANDROID_SDK_ROOT/ANDROID_HOME must be set. In a devbox shell, the flake-provided SDK should supply sdkmanager in PATH; if not, set ANDROID_SDK_ROOT to the flake's android-sdk path." >&2 + exit 1 + fi + + export ANDROID_HOME="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" + + require_tool avdmanager + require_tool emulator + + local primary_api="${AVD_API:-${ANDROID_MIN_API:-${PLATFORM_ANDROID_MIN_API:-21}}}" + local primary_tag="${AVD_TAG:-${ANDROID_SYSTEM_IMAGE_TAG:-${PLATFORM_ANDROID_SYSTEM_IMAGE_TAG:-google_apis}}}" + local primary_device="${AVD_DEVICE:-pixel}" + local primary_preferred_abi="${AVD_ABI:-}" + + local secondary_api="${AVD_SECONDARY_API:-${ANDROID_MAX_API:-${PLATFORM_ANDROID_MAX_API:-33}}}" + local secondary_tag="${AVD_SECONDARY_TAG:-${ANDROID_SYSTEM_IMAGE_TAG:-${PLATFORM_ANDROID_SYSTEM_IMAGE_TAG:-google_apis}}}" + local secondary_device="${AVD_SECONDARY_DEVICE:-medium_phone}" + local secondary_preferred_abi="${AVD_SECONDARY_ABI:-}" + + local targets=( + "$primary_api|$primary_tag|$primary_device|$primary_preferred_abi|${AVD_NAME:-}" + "$secondary_api|$secondary_tag|$secondary_device|$secondary_preferred_abi|${AVD_SECONDARY_NAME:-}" + ) + + for target in "${targets[@]}"; do + IFS="|" read -r api tag device preferred_abi name_override <<<"$target" + + local api_image + if ! api_image="$(pick_image "$api" "$tag" "$preferred_abi")"; then + echo "Expected API ${api} system image (${tag}; preferred ABI ${preferred_abi:-auto}) not found under ${ANDROID_SDK_ROOT}/system-images/android-${api}." >&2 + echo "Re-enter the devbox shell (flake should provide images) or rebuild Devbox to fetch them." >&2 + continue + fi + + local abi="${api_image##*;}" + local avd_name="${name_override:-$(printf '%s_API%s_%s' "$device" "$api" "${abi//-/_}")}" + + create_avd "$avd_name" "$device" "$api_image" + if avd_exists "$avd_name"; then + echo "AVD ready: ${avd_name} (${api_image})" + fi + done + + echo "AVDs ready. Boot with: emulator -avd --netdelay none --netspeed full" +} + +main "$@" diff --git a/scripts/ios-manager.sh b/scripts/ios-manager.sh new file mode 100755 index 000000000..5f3dcad5f --- /dev/null +++ b/scripts/ios-manager.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$script_dir/platform-versions.sh" ]; then + # shellcheck disable=SC1090 + . "$script_dir/platform-versions.sh" +fi + +action="${1:-}" +shift || true + +start_ios() { + local flavor="${IOS_FLAVOR:-latest}" + if [[ "$flavor" == "minsdk" ]]; then + export IOS_DEVICE_NAMES="${IOS_MIN_DEVICE:-${PLATFORM_IOS_MIN_DEVICE:-iPhone 13}}" + export IOS_RUNTIME="${IOS_MIN_RUNTIME:-${PLATFORM_IOS_MIN_RUNTIME:-15.0}}" + export DETOX_IOS_DEVICE="${DETOX_IOS_DEVICE:-${IOS_MIN_DEVICE:-${PLATFORM_IOS_MIN_DEVICE:-iPhone 13}}}" + else + export IOS_DEVICE_NAMES="${IOS_DEVICE_NAMES:-${IOS_MIN_DEVICE:-${PLATFORM_IOS_MIN_DEVICE:-iPhone 13}},${IOS_MAX_DEVICE:-${PLATFORM_IOS_MAX_DEVICE:-iPhone 17}}}" + export IOS_RUNTIME="${IOS_RUNTIME:-${IOS_MAX_RUNTIME:-${PLATFORM_IOS_MAX_RUNTIME:-}}}" + export DETOX_IOS_DEVICE="${DETOX_IOS_DEVICE:-iPhone 17}" + fi + + devbox run setup-ios + local sim_device="${DETOX_IOS_DEVICE}" + if ! xcrun simctl list devices | grep -q "${sim_device}"; then + echo "Simulator ${sim_device} not found; ensure setup-ios created it." >&2 + exit 1 + fi + echo "Starting iOS simulator: ${sim_device} (runtime ${IOS_RUNTIME})" + xcrun simctl boot "$sim_device" || true + open -a Simulator +} + +stop_ios() { + devbox run stop-ios +} + +reset_ios() { + devbox run reset-ios +} + +case "$action" in + start) start_ios ;; + stop) stop_ios ;; + reset) reset_ios ;; + *) echo "Usage: ios-manager.sh {start|stop|reset}" >&2; exit 1 ;; +esac diff --git a/scripts/ios-setup.sh b/scripts/ios-setup.sh new file mode 100755 index 000000000..2ed9bc0b0 --- /dev/null +++ b/scripts/ios-setup.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$script_dir/platform-versions.sh" ]; then + # shellcheck disable=SC1090 + . "$script_dir/platform-versions.sh" +fi + +# Creates local iOS simulators for common targets. Requires Xcode command-line tools and jq. +# Env overrides: +# IOS_DEVICE_NAMES="iPhone 15,iPhone 17" (comma-separated) +# IOS_RUNTIME="26.1" (preferred runtime prefix; falls back to latest available) +# IOS_DOWNLOAD_RUNTIME=1 to attempt xcodebuild -downloadPlatform iOS when the preferred runtime is missing +# IOS_DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" to override the Xcode path; defaults to xcode-select -p or the standard Xcode.app if found + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required tool: $1. Install Xcode CLI tools before running (xcode-select --install or Xcode.app + xcode-select -s)." >&2 + exit 1 + fi +} + +ensure_developer_dir() { + local desired="${IOS_DEVELOPER_DIR:-}" + if [[ -z "$desired" ]]; then + if xcode-select -p >/dev/null 2>&1; then + desired="$(xcode-select -p)" + elif [[ -d /Applications/Xcode.app/Contents/Developer ]]; then + desired="/Applications/Xcode.app/Contents/Developer" + fi + fi + + if [[ -n "$desired" && -d "$desired" ]]; then + export DEVELOPER_DIR="$desired" + export PATH="$DEVELOPER_DIR/usr/bin:$PATH" + return 0 + fi + + echo "Xcode developer directory not found. Install Xcode/CLI tools or set IOS_DEVELOPER_DIR to an Xcode path (e.g., /Applications/Xcode.app/Contents/Developer)." >&2 + exit 1 +} + +ensure_developer_dir + +require_tool xcrun +require_tool jq + +ensure_simctl() { + if xcrun -f simctl >/dev/null 2>&1; then + return 0 + fi + cat >&2 <<'EOF' +Missing simctl. +- The standalone Command Line Tools do NOT include simctl; you need full Xcode. +- Install/locate Xcode.app, then select it: + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer +- You can also set IOS_DEVELOPER_DIR to your Xcode path for this script. +EOF + exit 1 +} + +ensure_simctl + +ensure_core_sim_service() { + local output status + output="$(xcrun simctl list devices -j 2>&1)" || status=$? + if [[ -n "${status:-}" ]]; then + echo "simctl failed while listing devices (status ${status}). CoreSimulatorService may be unhealthy." >&2 + echo "Try restarting it:" >&2 + echo " killall -9 com.apple.CoreSimulatorService 2>/dev/null || true" >&2 + echo " launchctl kickstart -k gui/$UID/com.apple.CoreSimulatorService" >&2 + echo "Then open Simulator once and rerun devbox run setup-ios." >&2 + echo "simctl error output:" >&2 + echo "$output" >&2 + return 1 + fi + + if echo "$output" | grep -q "CoreSimulatorService connection became invalid"; then + echo "CoreSimulatorService is not healthy. Try restarting it:" >&2 + echo " killall -9 com.apple.CoreSimulatorService 2>/dev/null || true" >&2 + echo " launchctl kickstart -k gui/$UID/com.apple.CoreSimulatorService" >&2 + echo "Then open Simulator once and rerun devbox run setup-ios." >&2 + echo "simctl error output:" >&2 + echo "$output" >&2 + return 1 + fi +} + +pick_runtime() { + local preferred="$1" + local json choice + json="$(xcrun simctl list runtimes -j)" + choice="$(echo "$json" | jq -r --arg v "$preferred" '.runtimes[] | select(.isAvailable and (.name|startswith("iOS \($v)"))) | "\(.identifier)|\(.name)"' | head -n1)" + if [[ -z "$choice" || "$choice" == "null" ]]; then + choice="$(echo "$json" | jq -r '.runtimes[] | select(.isAvailable and (.name|startswith("iOS "))) | "\(.version)|\(.identifier)|\(.name)"' | sort -Vr | head -n1 | cut -d"|" -f2-)" + fi + [[ -n "$choice" && "$choice" != "null" ]] || return 1 + echo "$choice" +} + +resolve_runtime() { + local preferred="$1" + if choice=$(pick_runtime "$preferred"); then + echo "$choice" + return 0 + fi + + if [[ "${IOS_DOWNLOAD_RUNTIME:-1}" != "0" ]] && command -v xcodebuild >/dev/null 2>&1; then + echo "Preferred runtime iOS ${preferred} not found. Attempting to download via xcodebuild -downloadPlatform iOS..." >&2 + if xcodebuild -downloadPlatform iOS; then + if choice=$(pick_runtime "$preferred"); then + echo "$choice" + return 0 + fi + else + echo "xcodebuild -downloadPlatform iOS failed; continuing with available runtimes." >&2 + fi + fi + + pick_runtime "$preferred" +} + +existing_device_udid_any_runtime() { + local name="$1" + xcrun simctl list devices -j | jq -r --arg name "$name" '.devices[]?[]? | select(.name == $name) | .udid' | head -n1 +} + +device_data_dir_exists() { + local udid="${1:-}" + [[ -n "$udid" ]] || return 1 + local dir="$HOME/Library/Developer/CoreSimulator/Devices/$udid" + [[ -d "$dir" ]] +} + +devicetype_id_for_name() { + local name="$1" + xcrun simctl list devicetypes -j | jq -r --arg name "$name" '.devicetypes[] | select((.name|ascii_downcase) == ($name|ascii_downcase)) | .identifier' | head -n1 +} + +ensure_device() { + local base_name="$1" preferred_runtime="$2" + + # If a device with this name already exists anywhere, reuse it. + if existing_udid=$(existing_device_udid_any_runtime "$base_name"); [[ -n "${existing_udid}" ]]; then + if device_data_dir_exists "$existing_udid"; then + echo "Found existing ${base_name}: ${existing_udid}" + return 0 + fi + echo "Existing ${base_name} (${existing_udid}) is missing its data directory. Deleting stale simulator..." + xcrun simctl delete "$existing_udid" || true + fi + + local choice runtime_id runtime_name + if ! choice=$(resolve_runtime "$preferred_runtime"); then + echo "No available iOS simulator runtime found. Install one in Xcode (Settings > Platforms) and retry." >&2 + return 1 + fi + runtime_id="$(echo "$choice" | cut -d'|' -f1)" + runtime_name="$(echo "$choice" | cut -d'|' -f2)" + + local display_name="${base_name} (${runtime_name})" + + if ! device_type=$(devicetype_id_for_name "$base_name"); then + echo "Device type '${base_name}' is unavailable in this Xcode install. Skipping ${display_name}." >&2 + return 0 + fi + + # Also check for an existing device with the runtime-qualified display name. + if existing_udid=$(existing_device_udid_any_runtime "$display_name"); [[ -n "${existing_udid}" ]]; then + if device_data_dir_exists "$existing_udid"; then + echo "Found existing ${display_name}: ${existing_udid}" + return 0 + fi + echo "Existing ${display_name} (${existing_udid}) is missing its data directory. Deleting stale simulator..." + xcrun simctl delete "$existing_udid" || true + fi + + echo "Creating ${display_name}..." + xcrun simctl create "$display_name" "$device_type" "$runtime_id" + echo "Created ${display_name}" +} + +main() { + ensure_core_sim_service || return 1 + IFS=',' read -r -a devices <<<"${IOS_DEVICE_NAMES:-${IOS_MIN_DEVICE:-${PLATFORM_IOS_MIN_DEVICE:-iPhone 13}},${IOS_MAX_DEVICE:-${PLATFORM_IOS_MAX_DEVICE:-iPhone 17}}}" + local runtime="${IOS_RUNTIME:-${IOS_MIN_RUNTIME:-${PLATFORM_IOS_MIN_RUNTIME:-15.0}}}" + for device in "${devices[@]}"; do + ensure_device "$(echo "$device" | xargs)" "$runtime" + done + echo "Done. Launch via Xcode > Devices or 'xcrun simctl boot \"\"' then 'open -a Simulator'." +} + +main "$@" diff --git a/scripts/platform-versions.sh b/scripts/platform-versions.sh new file mode 100644 index 000000000..183968da6 --- /dev/null +++ b/scripts/platform-versions.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +# Shared platform version defaults for Android/iOS. + +PLATFORM_ANDROID_MIN_API="21" +PLATFORM_ANDROID_MAX_API="33" +PLATFORM_ANDROID_BUILD_TOOLS_VERSION="30.0.3" +PLATFORM_ANDROID_CMDLINE_TOOLS_VERSION="19.0" +PLATFORM_ANDROID_SYSTEM_IMAGE_TAG="google_apis" +PLATFORM_ANDROID_MIN_DEVICE="pixel" +PLATFORM_ANDROID_MAX_DEVICE="medium_phone" + +PLATFORM_IOS_MIN_RUNTIME="15.0" +PLATFORM_IOS_MAX_RUNTIME="" +PLATFORM_IOS_MIN_DEVICE="iPhone 13" +PLATFORM_IOS_MAX_DEVICE="iPhone 17" diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 000000000..45cca9cc9 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,13 @@ +[formatter.nix] +command = "nixfmt" +includes = ["*.nix"] + +[formatter.prettier] +command = "prettier" +options = ["--write"] +includes = ["*.js", "*.jsx", "*.ts", "*.tsx", "*.md", "*.yml", "*.yaml", "*.json"] + +[formatter.shfmt] +command = "shfmt" +options = ["-w", "-s", "-i", "2"] +includes = ["*.sh"] diff --git a/wiki/devbox.md b/wiki/devbox.md new file mode 100644 index 000000000..957ea1a92 --- /dev/null +++ b/wiki/devbox.md @@ -0,0 +1,71 @@ +## Devbox Overview + +This repo ships a Devbox environment that preinstalls the Android SDK and common build tools like Gradle and Yarn. Devbox uses Nix under the hood to pin versions so everyone has the same setup. You don’t need to know Nix to use it. + +Enter the environment with `devbox shell`. The init hook wires `ANDROID_SDK_ROOT`/`ANDROID_HOME` and PATH. Common scripts run via `devbox run`, for example `devbox run build`. For Devbox basics, see the official docs: https://www.jetify.com/devbox/docs/. + +## Getting started + +- Install Devbox (https://www.jetify.com/devbox/docs/install/). +- From the repo root: `devbox install`. +- Enter the shell: `devbox shell`. +- Build/test: `devbox run build`, `devbox run test-android`, `devbox run test-ios`. + +## Android + +By default, Devbox uses the flake-pinned SDK (`path:./nix#android-sdk`). It sets `ANDROID_SDK_ROOT`/`ANDROID_HOME` and adds emulator/platform-tools/cmdline-tools to `PATH` via `scripts/android-env.sh`. Platform versions live in `scripts/platform-versions.sh` (source of truth for min/max API and build tools). To use a local SDK instead, launch with `ANDROID_HOME="$HOME/Library/Android/sdk" devbox shell` (or set `ANDROID_SDK_ROOT`). Clear both env vars to return to the Nix SDK. Inspect the active SDK with `echo "$ANDROID_SDK_ROOT"` and `which sdkmanager` inside the shell. Create/boot AVDs via `devbox run start-android*` (uses `scripts/android-setup.sh` + `scripts/android-manager.sh`). + +### Emulator/AVD scripts + +- `devbox run start-android` launches the default “latest” AVD (API 33, Medium Phone). On arm64 hosts it uses the arm64-v8a image; on Intel it uses x86_64. Override with `AVD_FLAVOR=minsdk` to launch the API 21 Pixel AVD instead. You can also set `DETOX_AVD` to pick an exact AVD name. +- `devbox run start-android-latest` / `start-android-minsdk` explicitly launch the latest (API 33) or minsdk (API 21) AVDs. Both will create the AVD first via `scripts/android-setup.sh` if it does not exist. +- `scripts/android-setup.sh` now accepts env overrides: `AVD_API`, `AVD_DEVICE`, `AVD_TAG`, `AVD_ABI`, `AVD_NAME`. Defaults target API 21 for minsdk; CI passes API 33 for latest. The script auto-selects the best ABI for the host (arm64-v8a on arm, x86_64 on Intel) if `AVD_ABI` is unset. +- `devbox run reset-android` removes local AVDs/adb keys if you need a clean slate. +- `EMU_HEADLESS=1 devbox run start-android*` to run the emulator headless (CI sets this); omit for a visible emulator locally. +- `EMU_PORT=5554 devbox run start-android*` to set the emulator port/serial (defaults to 5554) and avoid adb conflicts. +- `devbox run test-android` runs `setup-android` first to ensure AVDs exist, then delegates startup to Detox. It will not boot an emulator itself; use `start-android*` if you want it running beforehand. + +### Detox defaults + +- Android Detox defaults to the latest AVD (`medium_phone_API33_arm64_v8a` on arm hosts, x86_64 otherwise). Set `DETOX_AVD=pixel_API21_*` to run against the minsdk AVD. +- CI Android E2E runs both API 21 (Pixel) and API 33 (Medium Phone) in parallel in the nightly workflow. Override the workflow matrix in `ci-e2e-nightly.yml` if needed. + +### Updating Android min/latest versions + +- Bump pinned SDKs in `nix/flake.nix` (platformVersions/buildToolsVersions/cmdLineToolsVersion). Rebuild devbox (`devbox shell --rebuild`) so everyone gets the new SDK. +- Update AVD defaults/names if you change API levels: + - `devbox.json` (`start-android-*` scripts) for default AVD names. + - `examples/E2E/.detoxrc.js` for the default `DETOX_AVD`. + - CI matrix in `.github/workflows/ci.yml` (`ANDROID_MATRIX` entries). +- Gradle uses `buildToolsVersion` from `examples/E2E/android/build.gradle`; Devbox exports `ANDROID_BUILD_TOOLS_VERSION` from `scripts/platform-versions.sh` (single source of truth) and you can override it if needed. + +## iOS + +iOS uses the host Xcode toolchain. There is no Nix-provisioned iOS SDK. Run `devbox run setup-ios` to install pods and bootstrap the iOS example/E2E apps. Full Xcode is required for `simctl` (Command Line Tools alone are not enough). Make sure Xcode command line tools are selected (`xcode-select --print-path` or `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`) and that you have agreed to the license if prompted. + +> Important: `devbox run` by itself uses the Nix compiler toolchain on macOS. For iOS work you must route commands through `devbox shell --omit-nix-env --command ""` (or run an interactive `devbox shell --omit-nix-env` first) so the Xcode toolchain is used. CI does this for setup/tests. + +### Simulators and Detox + +- `devbox run setup-ios` provisions simulators. Defaults are driven by `scripts/platform-versions.sh` (min/max device/runtime). Override via env vars to target a specific device/runtime. Set `IOS_DOWNLOAD_RUNTIME=0` to skip attempting `xcodebuild -downloadPlatform iOS` when the preferred runtime is missing. Set `IOS_DEVELOPER_DIR` (e.g., `/Applications/Xcode.app/Contents/Developer`) to point at a specific Xcode; otherwise it uses `xcode-select -p` or the default Xcode.app if found. +- On macOS, use `devbox shell --omit-nix-env --command ""` when invoking iOS builds/tests to ensure the Xcode toolchain is used instead of the Nix compiler toolchain. +- `devbox run start-ios` provisions simulators (via `setup-ios`), then boots the chosen device (`DETOX_IOS_DEVICE` or default `iPhone 17`) and opens Simulator. Set `IOS_FLAVOR=minsdk` to target the min sim (per `scripts/platform-versions.sh`) or leave default for latest. Internally uses `scripts/ios-manager.sh`. +- `devbox run reset-ios` shuts down/erases and removes all local simulator devices. +- `devbox run stop-android` / `stop-ios` / `stop` to shut down running emulators/simulators (handy for headless runs). +- Detox defaults to `iPhone 17` for local runs; override with `DETOX_IOS_DEVICE`. CI runs a matrix: min sim (from `scripts/platform-versions.sh`) and latest (iPhone 17 @ latest runtime). +- `devbox run test-ios` runs `setup-ios` first to ensure simulators exist; Detox handles booting. Use `start-ios` if you want to pre-boot. + +### Common env knobs + +- Android: `AVD_FLAVOR` (minsdk/latest), `DETOX_AVD` (explicit AVD name), `EMU_HEADLESS` (1 for headless), `EMU_PORT` (emulator port/serial), `ANDROID_BUILD_TOOLS_VERSION` (override build-tools). +- iOS: `IOS_FLAVOR` (minsdk/latest), `DETOX_IOS_DEVICE` (explicit sim device), `IOS_RUNTIME` (preferred runtime), `IOS_DEVICE_NAMES` (comma list to create), `IOS_DEVELOPER_DIR` (Xcode path), `IOS_DOWNLOAD_RUNTIME` (0 to skip runtime download attempt). + +### Releases + +- `devbox run release` runs npm auth, yarn install/build, and `yarn release`. Requires `NPM_TOKEN` (and `GH_TOKEN`/`YARN_NPM_AUTH_TOKEN` for publishing). Used by the publish workflow. + +### Updating iOS min/latest versions + +- Adjust platform defaults in `scripts/platform-versions.sh` and rebuild Devbox if you change Android SDK versions. +- Update Detox default device in `examples/E2E/.detoxrc.js` if the default device changes. +- Update CI matrices in `.github/workflows/ci-e2e-nightly.yml` (ios-min/ios-latest rows) if you want to override the platform defaults in CI. diff --git a/wiki/release.md b/wiki/release.md new file mode 100644 index 000000000..1b24c48a1 --- /dev/null +++ b/wiki/release.md @@ -0,0 +1,28 @@ +## Release guide + +This repo uses semantic-release with multi-semantic-release to version and publish all public workspaces. Tags follow `${name}-v${version}` and releases are cut from `master` (stable) and `beta` (prerelease). + +### Prerequisites +- Secrets: `GH_TOKEN` (repo `contents` write) and `NPM_TOKEN` (publish). CI also passes `YARN_NPM_AUTH_TOKEN` (same as `NPM_TOKEN`). +- Git history: full clone (`fetch-depth: 0`) so semantic-release can find prior tags. +- Commit format: conventional commits; commitlint is already configured. + +### What runs +- Config files: `release.config.js` (single-package defaults) and `multi-release.config.js` (multi-package orchestration, sequential init/prepare, ignore private packages, tag format/branches). +- Plugins: commit analyzer + release notes, changelog (`CHANGELOG.md`), npm publish, GitHub release (no success comment), and git commit of changelog + package.json. +- Script: root `yarn release` runs `multi-semantic-release` with the above config per public package. + +### CI/CD path (recommended) +1) Ensure `master`/`beta` are green. Merges must use conventional commits. +2) Trigger `Publish` workflow in Actions. Inputs are tokens only; workflow fetches full history, installs Devbox, then runs `devbox run release`. +3) Outputs: package tags (`${name}-vX.Y.Z`), npm publishes, GitHub releases, and updated changelog commits pushed back via the workflow token. + +### Local dry run +1) `GH_TOKEN= NPM_TOKEN= YARN_NPM_AUTH_TOKEN=` (GH token needs `contents` write; npm token can be automation/classic publish). +2) `devbox run release -- --dry-run` to see what would publish. Omit `--dry-run` to actually publish (only do this if you intend to release from your machine). + +### Tips and gotchas +- Only public packages release; private workspaces (e.g., `packages/shared`) are ignored. +- Tag pattern is important: keep `${name}-v${version}` if you create manual tags for debugging. +- If adding a new branch for releases, update both `release.config.js` and `multi-release.config.js`. +- Keep yarn.lock in sync before releasing to avoid install differences between CI and local. diff --git a/yarn.lock b/yarn.lock index 4767704ef..3eee16575 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3380,7 +3380,7 @@ __metadata: version: 0.0.0-use.local resolution: "@segment/analytics-react-native-plugin-amplitude-session@workspace:packages/plugins/plugin-amplitudeSession" dependencies: - "@segment/analytics-react-native": "npm:^2.18.0" + "@segment/analytics-react-native": "npm:^2.21.4" "@segment/analytics-rn-shared": "workspace:^" "@segment/sovran-react-native": "npm:^1.1.0" "@types/jest": "npm:^29.5.8" @@ -3615,7 +3615,7 @@ __metadata: languageName: unknown linkType: soft -"@segment/analytics-react-native@npm:^2.18.0, @segment/analytics-react-native@workspace:packages/core": +"@segment/analytics-react-native@npm:^2.18.0, @segment/analytics-react-native@npm:^2.21.4, @segment/analytics-react-native@workspace:packages/core": version: 0.0.0-use.local resolution: "@segment/analytics-react-native@workspace:packages/core" dependencies: