Skip to content
Getting Started

React Native Quickstart

Place the Shipfile correctly for React Native and Expo, then archive both platforms.

Get your first React Native (including Expo) build shipped with ShipItSwifty.

react_native is a fully supported build_system value — it drives build, archive, test, and lint on both platforms — but it is easy to configure incorrectly on the first try because a React Native project has two independent platform sub-projects (android/, ios/) nested under one JavaScript root. This guide places the Shipfile correctly the first time.

Prerequisites

  • Xcode CLI tools or Homebrew (swift, swift build)
  • A React Native (bare or Expo) project with react-native in package.json dependencies
  • For Android: the project's android/gradlew wrapper (generated by expo prebuild for managed Expo apps, or present from the start in a bare RN app)
  • For iOS: ios/<App>.xcworkspace (present the same way)

1. Install ShipItSwifty

git clone https://github.com/shipitswifty/shipitswifty
cd shipitswifty
swift build -c release
cp .build/release/shipit /usr/local/bin/shipit

2. Put the Shipfile at the React Native root — not in android/ or ios/

Run every shipit command from the directory that holds package.json, not from android/ or ios/:

cd /path/to/MyReactNativeApp   # the directory with package.json
shipit generate --goal beta --platform android --non-interactive

This matters because build_system auto-detection reads package.json to recognize a React Native project, and that file only exists at the RN root. generate writes platform: android (or ios) and build_system: react_native under that platform's block, and leaves android.gradlew_path / android.gradle_project_dir at their defaults — which resolve to ./android relative to the Shipfile's own directory. Do not set gradle_project_dir for a React Native project. It only selects which gradlew script runs; it does not relocate where Gradle looks for settings.gradle. The RN-aware default already points at the right place.

shipit generate targets one platform per invocation (--platform ios or --platform android). For a React Native project that ships both, run it twice into two files:

shipit generate --goal beta --platform android --non-interactive
mv Shipfile.yml Shipfile.android.yml
shipit generate --goal beta --platform ios --non-interactive
mv Shipfile.yml Shipfile.ios.yml

and select one with --shipfile per invocation (shipit run beta --shipfile Shipfile.android.yml). A single Shipfile can only resolve one platform at a time — there is no per-step platform: option and no supported way to mix ios: and android: workflows validly in one file.

3. Managed Expo: keep the Shipfile out of android/ for a different reason

For a bare RN project, android/ and ios/ are checked in — either RN-root or in-directory placement is committable. For a managed Expo project, android/ and ios/ are expo prebuild output and typically gitignored (check .gitignore — Expo's default template adds /android/ and /ios/ outright). A Shipfile placed inside android/ for that reason would be ignored by git and destroyed by the next expo prebuild --clean. Keeping the Shipfile at the RN root avoids both problems and is the layout generate produces by default.

4. Cap Gradle on memory-constrained machines

React Native Android builds compile native code (Hermes, JSC, any custom native modules) via CMake/ninja in addition to the Gradle daemon itself. GRADLE_OPTS and android.gradle_flags cap Gradle's own worker count, but the CMake sub-build chooses its own parallelism independently — on an 8GB machine building all four ABIs at once, this can still push the machine into swap. Two independent levers, both worth setting:

GRADLE_OPTS="-Dorg.gradle.workers.max=2 -Dorg.gradle.parallel=false"
android:
  gradle_flags: [--no-parallel]
  gradle_properties:
    reactNativeArchitectures: arm64-v8a   # build one ABI instead of all four locally

5. Archive

shipit archive --platform android
shipit archive --platform ios

Android: ShipIt first tries npx react-native build-android --mode=release --tasks bundleRelease. On a managed Expo app without @react-native-community/cli installed, that command is unavailable — ShipIt detects the specific failure message and falls back to a direct gradlew :app:bundleRelease automatically. No configuration is needed for this fallback; it is the default behavior.

iOS: the RN build system falls back to scanning <projectRoot>/ios for a workspace when app.workspace is unset, so ios/MyApp.xcworkspace is found without configuring it explicitly. archive defaults to writing ./build/<Scheme>.xcarchive relative to the Shipfile's directory (the RN root) — add ./build/ to .gitignore if it isn't already covered, since a .xcarchive is not small.

6. Local (unsigned) builds without provisioning

To validate that archive/build work before setting up signing, pass code-signing overrides through build.xcargs (raw KEY=VALUE build settings, applied verbatim to xcodebuild):

build:
  xcargs:
    CODE_SIGNING_ALLOWED: "NO"
    CODE_SIGNING_REQUIRED: "NO"
    CODE_SIGN_IDENTITY: ""

xcargs is a [String: String] map — it can express KEY=VALUE build settings but not bare xcodebuild flags (e.g. there is no way to pass -jobs through it).

7. Workflow naming

generate names the workflow it writes beta (or local/release) for a single-platform Shipfile — matching shipit run beta from the rest of the documentation. If you hand-authored a Shipfile with a different workflow name, shipit run beta will fail with a list of the workflows that do exist; use that name instead of guessing.

What's not React Native-specific

Everything after archiving — export, testflight, play-store, upload, code signing setup — works exactly as documented for native iOS/Android projects once you have an .xcarchive or .aab. See the Configuration reference for the full build-system matrix and the iOS Quickstart for signing and TestFlight.