Back to Blog

CI/CD for Mobile App Development: Complete Guide 2026

Complete guide to CI/CD for mobile apps. Learn to automate builds, testing, and deployment for iOS, Android, React Native, and Flutter apps.

Hevin GolakiyaFounder, Hevcode
Published January 24, 2026

Continuous Integration and Continuous Deployment (CI/CD) automates building, testing, and deploying mobile apps. This guide covers setting up CI/CD pipelines for iOS, Android, React Native, and Flutter applications.

Why CI/CD for Mobile?

  • Faster releases - Automate repetitive tasks
  • Fewer bugs - Automated testing catches issues early
  • Consistent builds - Same process every time
  • Team efficiency - Developers focus on code, not deployment

CI/CD Pipeline Overview

┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│  Code   │───▶│  Build  │───▶│  Test   │───▶│ Deploy  │───▶│ Release │
│  Push   │    │         │    │         │    │ (Beta)  │    │ (Store) │
└─────────┘    └─────────┘    └─────────┘    └─────────┘    └─────────┘
     │              │              │              │              │
     ▼              ▼              ▼              ▼              ▼
  Trigger       Compile       Unit Tests    TestFlight     App Store
  Pipeline      & Sign        UI Tests      Play Console   Play Store

Popular CI/CD Platforms

Platform Best For iOS Android Price
GitHub Actions GitHub users Yes Yes Free tier
Bitrise Mobile-first Yes Yes Free tier
Codemagic Flutter Yes Yes Free tier
CircleCI Large teams Yes Yes Free tier
Fastlane Local/any CI Yes Yes Free

GitHub Actions Setup

React Native CI/CD

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run TypeScript check
        run: npm run typecheck

      - name: Run unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  build-android:
    needs: lint-and-test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Install dependencies
        run: npm ci

      - name: Build Android Release
        working-directory: android
        run: ./gradlew assembleRelease

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: app-release.apk
          path: android/app/build/outputs/apk/release/app-release.apk

  build-ios:
    needs: lint-and-test
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install CocoaPods
        working-directory: ios
        run: pod install

      - name: Build iOS
        working-directory: ios
        run: |
          xcodebuild -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -configuration Release \
            -sdk iphonesimulator \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            build

Android Deployment to Play Store

# .github/workflows/android-release.yml
name: Android Release

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Install dependencies
        run: npm ci

      - name: Decode Keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore

      - name: Build Release Bundle
        working-directory: android
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew bundleRelease

      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
          packageName: com.myapp
          releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
          track: internal
          status: completed

iOS Deployment to TestFlight

# .github/workflows/ios-release.yml
name: iOS Release

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install CocoaPods
        working-directory: ios
        run: pod install

      - name: Install Apple Certificate
        uses: apple-actions/import-codesign-certs@v2
        with:
          p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
          p12-password: ${{ secrets.CERTIFICATES_PASSWORD }}

      - name: Install Provisioning Profile
        uses: apple-actions/download-provisioning-profiles@v1
        with:
          bundle-id: com.myapp
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

      - name: Build and Archive
        working-directory: ios
        run: |
          xcodebuild -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -configuration Release \
            -archivePath build/MyApp.xcarchive \
            archive

      - name: Export IPA
        working-directory: ios
        run: |
          xcodebuild -exportArchive \
            -archivePath build/MyApp.xcarchive \
            -exportPath build \
            -exportOptionsPlist ExportOptions.plist

      - name: Upload to TestFlight
        uses: apple-actions/upload-testflight-build@v1
        with:
          app-path: ios/build/MyApp.ipa
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

Flutter CI/CD

# .github/workflows/flutter-ci.yml
name: Flutter CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'
          cache: true

      - name: Install dependencies
        run: flutter pub get

      - name: Analyze code
        run: flutter analyze

      - name: Run tests
        run: flutter test --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3

  build-android:
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Build APK
        run: flutter build apk --release

      - name: Build App Bundle
        run: flutter build appbundle --release

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: app-release.apk
          path: build/app/outputs/flutter-apk/app-release.apk

  build-ios:
    needs: test
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Build iOS (no signing)
        run: flutter build ios --release --no-codesign

Fastlane Configuration

Fastlane automates iOS and Android deployment locally or in CI.

iOS Fastfile

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Run tests"
  lane :test do
    run_tests(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      devices: ["iPhone 15"]
    )
  end

  desc "Build and upload to TestFlight"
  lane :beta do
    # Increment build number
    increment_build_number(
      build_number: ENV["BUILD_NUMBER"] || latest_testflight_build_number + 1
    )

    # Build app
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      export_method: "app-store"
    )

    # Upload to TestFlight
    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )
  end

  desc "Deploy to App Store"
  lane :release do
    # Ensure we're on main branch
    ensure_git_branch(branch: "main")

    # Build app
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      export_method: "app-store"
    )

    # Upload to App Store
    upload_to_app_store(
      submit_for_review: true,
      automatic_release: true,
      force: true
    )
  end
end

Android Fastfile

# android/fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Run tests"
  lane :test do
    gradle(task: "test")
  end

  desc "Build and upload to Play Store Internal"
  lane :beta do
    # Increment version code
    increment_version_code(
      gradle_file_path: "app/build.gradle"
    )

    # Build release bundle
    gradle(
      task: "bundle",
      build_type: "Release"
    )

    # Upload to Play Store
    upload_to_play_store(
      track: "internal",
      aab: "app/build/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Promote Internal to Production"
  lane :release do
    upload_to_play_store(
      track: "internal",
      track_promote_to: "production",
      skip_upload_aab: true,
      skip_upload_metadata: true
    )
  end
end

Environment Management

Using Environment Variables

# GitHub Actions secrets
env:
  API_URL: ${{ secrets.API_URL }}
  API_KEY: ${{ secrets.API_KEY }}

# Create .env file during build
- name: Create .env file
  run: |
    echo "API_URL=${{ secrets.API_URL }}" >> .env
    echo "API_KEY=${{ secrets.API_KEY }}" >> .env

React Native Environment Config

// react-native-config
// .env.production
API_URL=https://api.production.com
API_KEY=prod_key_xxx

// .env.staging
API_URL=https://api.staging.com
API_KEY=staging_key_xxx

// Usage
import Config from 'react-native-config';
const apiUrl = Config.API_URL;

Flutter Environment Config

// Using --dart-define
// flutter build apk --dart-define=API_URL=https://api.production.com

class Environment {
  static const String apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api.dev.com',
  );
}

Code Signing

iOS Code Signing with Match

# Matchfile
git_url("https://github.com/company/certificates")
storage_mode("git")
type("appstore") # or "development", "adhoc"
app_identifier(["com.myapp"])

# In Fastfile
lane :setup_signing do
  match(
    type: "appstore",
    readonly: is_ci
  )
end

Android Signing

// android/app/build.gradle
android {
    signingConfigs {
        release {
            storeFile file(System.getenv("KEYSTORE_FILE") ?: "release.keystore")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

Automated Version Management

Semantic Versioning

# .github/workflows/version.yml
name: Version Bump

on:
  push:
    branches: [main]

jobs:
  version:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Bump version and push tag
        id: tag_version
        uses: mathieudutour/github-tag-action@v6.1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          default_bump: patch

      - name: Create Release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ steps.tag_version.outputs.new_tag }}
          name: Release ${{ steps.tag_version.outputs.new_tag }}
          body: ${{ steps.tag_version.outputs.changelog }}

Auto-increment Build Number

# iOS - Fastlane
increment_build_number(
  build_number: ENV["GITHUB_RUN_NUMBER"]
)

# Android - Gradle
def getBuildNumber() {
    return System.getenv("GITHUB_RUN_NUMBER") ?: "1"
}

android {
    defaultConfig {
        versionCode getBuildNumber().toInteger()
    }
}

Best Practices

1. Cache Dependencies

# Node modules
- uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

# Gradle
- uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

# CocoaPods
- uses: actions/cache@v3
  with:
    path: ios/Pods
    key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}

2. Parallel Jobs

jobs:
  lint:
    runs-on: ubuntu-latest
    # ...

  test:
    runs-on: ubuntu-latest
    # ...

  build-android:
    needs: [lint, test]  # Run after lint and test
    runs-on: ubuntu-latest
    # ...

  build-ios:
    needs: [lint, test]  # Runs in parallel with build-android
    runs-on: macos-latest
    # ...

3. Fail Fast

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: true  # Stop all jobs if one fails
      matrix:
        node: [18, 20]

4. Branch Protection

Configure branch protection rules:

  • Require status checks before merging
  • Require branches to be up to date
  • Require code review approval

Conclusion

CI/CD is essential for modern mobile development. Start with basic automated testing, then add automated deployment. Use Fastlane for complex workflows and caching for faster builds.

Need help setting up CI/CD? Contact Hevcode for professional mobile app development with automated deployment pipelines.

Related Articles

Tags:CI/CDDevOpsMobile DevelopmentAutomationGitHub Actions

Need help with your project?

We've helped 534+ clients build successful apps. Let's discuss yours.

Ready to Build Your App?

534+ projects delivered • 273+ verified reviews • 6+ years experience

Let's discuss your project — no obligations, just a straightforward conversation.