Skip to content
Mehedi Hasan Sagor

// blog

CI/CD Blueprint for Ionic + React Native Apps: From Monorepo to Store

Mehedi Hasan Sagor Oct 22, 2026 4 min read
CI/CD Blueprint for Ionic + React Native Apps: From Monorepo to Store

Why CI/CD matters for a hybrid mobile codebase

When a startup ships a mobile product, speed and reliability are non‑negotiable. A monorepo that houses both an Ionic web view (usually Angular or React) and a native React Native layer introduces two distinct build pipelines, two sets of dependencies, and two deployment targets (iOS and Android). Manual builds quickly become a bottleneck: a missed version bump, a flaky test, or a forgotten signing key can delay a release by days.

A well‑engineered CI/CD pipeline solves three problems at once:

  • Consistency – Every commit goes through the same steps, guaranteeing that the app you test locally is the same you ship.
  • Fast feedback – Unit, component, and end‑to‑end tests run in parallel, catching regressions before they reach QA.
  • Automation of repetitive tasks – Code signing, version bumping, and store uploads happen without human intervention, reducing human error.

Below is a pragmatic blueprint that works with a typical Yarn‑based monorepo, GitHub Actions, and Fastlane. The concepts apply equally to GitLab CI, Azure Pipelines, or CircleCI.

Organising the monorepo for CI clarity

A clear folder layout makes the pipeline easier to understand and to cache.

my-app/
├─ packages/
│  ├─ ionic/          # Ionic project (React or Angular)
│  └─ rn/             # React Native project
├─ scripts/           # Helper scripts (version bump, lint, etc.)
├─ .github/workflows/ # CI definitions
└─ package.json       # Root Yarn workspace definition
  • Root package.json declares the workspaces so a single yarn install pulls all dependencies.
  • scripts/ contains tiny Node scripts that can be called from the CI without pulling the whole repo into a Docker image.
  • Separate ionic and rn packages keep platform‑specific config files (capacitor.config.json, android/app, ios/) isolated, which simplifies caching.

The GitHub Actions workflow

The following workflow runs on every push to main and on pull‑request updates. It builds, tests, and optionally releases when a tag is pushed.

name: CI/CD for Ionic + RN

on:
  push:
    branches: [main]
    tags: [v*]
  pull_request:
    branches: [main]

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      cache-key: ${{ steps.cache.outputs.cache-key }}
    steps:
      - uses: actions/checkout@v4
      - name: Cache node_modules
        id: cache
        uses: actions/cache@v3
        with:
          path: |
            **/node_modules
            ~/.cache/Yarn
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
      - name: Install dependencies
        run: yarn install --frozen-lockfile

  lint:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: |
            **/node_modules
            ~/.cache/Yarn
          key: ${{ needs.setup.outputs.cache-key }}
      - name: Lint
        run: yarn lint

  test:
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform: [ionic, rn]
    steps:
      - uses: actions/checkout@v4
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: |
            **/node_modules
            ~/.cache/Yarn
          key: ${{ needs.setup.outputs.cache-key }}
      - name: Run unit tests
        run: |
          if [ "${{ matrix.platform }}" = "ionic" ]; then
            yarn workspace ionic test
          else
            yarn workspace rn test
          fi

  build:
    needs: test
    runs-on: macos-latest
    strategy:
      matrix:
        platform: [ionic, rn]
    steps:
      - uses: actions/checkout@v4
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: |
            **/node_modules
            ~/.cache/Yarn
          key: ${{ needs.setup.outputs.cache-key }}
      - name: Build ${{ matrix.platform }}
        env:
          NODE_OPTIONS: --max_old_space_size=4096
        run: |
          if [ "${{ matrix.platform }}" = "ionic" ]; then
            cd packages/ionic && yarn build && npx cap copy ios && npx cap copy android
          else
            cd packages/rn && npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/main.jsbundle && cd ios && pod install && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -derivedDataPath build
          fi
      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: ${{ matrix.platform }}-build
          path: |
            packages/ionic/www/**
            packages/rn/ios/build/**
            packages/rn/android/app/build/outputs/apk/release/**

  release:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: build
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          path: artifacts
      - name: Set up Fastlane
        run: |
          gem install fastlane -NV
      - name: Deploy iOS
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          FASTLANE_USER: ${{ secrets.APPLE_ID }}
          FASTLANE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
        run: |
          cd packages/rn/ios
          fastlane ios release
      - name: Deploy Android
        env:
          GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
        run: |
          cd packages/rn/android
          fastlane android release

Key points in the workflow

  • Separate jobs for lint, test, and build – failures surface early, and each job can be retried independently.
  • MacOS runners for iOS builds – required for Xcode and Fastlane.
  • Caching node_modules dramatically reduces install time; the cache key includes the lockfile hash, so any dependency change invalidates it.
  • Matrix strategy – one job runs the Ionic pipeline, another runs the React Native pipeline, keeping logs tidy.

Testing strategy that scales with the monorepo

A hybrid project has three testing layers:

  1. Unit tests – Run with Jest (Ionic) and Jest/React‑Native‑Testing‑Library (RN). Keep them fast (<1 s per suite) so the CI matrix can execute them in parallel.
  2. Component / UI tests – For Ionic, use Cypress with the @cypress/react plugin; for RN, use Detox. Both can run in headless mode on CI.
  3. End‑to‑end device tests – Services like Firebase Test Lab or AWS Device Farm let you run the full app on a matrix of real devices. Trigger them only on main or when a release tag is created to keep PR feedback quick.

Example of a shared script (scripts/run-tests.js) that decides which suite to execute based on an environment variable:

// scripts/run-tests.js
const { execSync } = require('child_process');
const platform = process.env.PLATFORM;
if (platform === 'ionic') {
  execSync('yarn workspace ionic test:unit', { stdio: 'inherit' });
  execSync('yarn workspace ionic test:e2e', { stdio: 'inherit' });
} else if (platform === 'rn') {
  execSync('yarn workspace rn test:unit', { stdio: 'inherit' });
  execSync('yarn workspace rn test:detox', { stdio: 'inherit' });
} else {
  console.error('Unknown PLATFORM');
  process.exit(1);
}

In the CI file, you can call node scripts/run-tests.js after setting PLATFORM.

Automating versioning and code signing

Manual version bumps are a common source of human error. The standard-version package can generate a semver tag from conventional commit messages.

# Bump version and push tag (run on release branch only)
npm install -g standard-version
standard-version --release-as patch
git push --follow-tags origin main

For iOS, use Fastlane match to store certificates in a private Git repo. For Android, keep the google-play-service.json key in GitHub Secrets and let Fastlane decrypt it during the release job.

Deploying with Fastlane – a single source of truth

Fastlane lanes encapsulate the entire release flow. A minimal Fastfile for both platforms looks like this:

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    capture_screenshots
    build_app(scheme: "MyApp", export_method: "app-store")
    upload_to_testflight
  end

  desc "Release a new version to the App Store"
  lane :release do
    capture_screenshots
    build_app(scheme: "MyApp", export_method: "app-store")
    upload_to_app_store(skip_metadata: true, skip_screenshots: true)
  end
end

platform :android do
  desc "Deploy a new version to Google Play"
  lane :release do
    gradle(task: "assembleRelease")
    upload_to_play_store(track: "production")
  end
end

Running fastlane ios release or fastlane android release from the CI job completes the end‑to‑end flow: compile, sign, upload, and optionally notify Slack via a webhook.

Checklist for a production‑ready pipeline

  • [ ] Monorepo caching – node_modules, CocoaPods, Gradle caches.
  • [ ] Static analysis – ESLint for both packages, TypeScript strict mode.
  • [ ] Dependency audit – yarn audit as a separate job.
  • [ ] Secret management – All signing keys and API tokens stored in GitHub Secrets or a vault.
  • [ ] Rollback plan – Keep the previous build artifact for quick hot‑fixes; Fastlane can also promote a prior build.
  • [ ] Monitoring – Enable crash reporting (Sentry, Firebase Crashlytics) in the released binaries.

By wiring these pieces together, a startup can ship a new mobile version to both stores in under an hour, with confidence that the same code that passed CI is what users download.

Takeaway

Investing a few days to set up a robust CI/CD pipeline for a shared‑code Ionic + React Native monorepo pays off quickly. You gain repeatable builds, fast feedback on every pull request, and a zero‑touch release process that scales as your team grows. The template above is deliberately minimal—adapt it to your own tooling, but keep the core ideas of caching, parallel testing, automated versioning, and Fastlane‑driven deployment.

  • Ionic
  • React Native
  • Mobile
  • Devops
  • Ci