react-native-sop-docs/09-ci-cd-pipeline/github-actions-fastlane.md

12 KiB

CI/CD Pipeline

GitHub Actions Setup

Basic Workflow

.github/workflows/ci.yml:

name: CI/CD Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Run linter
        run: yarn lint
      
      - name: Run tests
        run: yarn test --coverage
      
      - name: Type check
        run: yarn type-check
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

  build-android:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'
      
      - name: Setup Android SDK
        uses: android-actions/setup-android@v2
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Cache Gradle
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper            
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
      
      - name: Build Android APK
        run: |
          cd android
          ./gradlew assembleRelease          
      
      - name: Upload APK
        uses: actions/upload-artifact@v3
        with:
          name: android-apk
          path: android/app/build/outputs/apk/release/app-release.apk

  build-ios:
    needs: test
    runs-on: macos-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Install CocoaPods
        run: |
          cd ios
          pod install          
      
      - name: Build iOS
        run: |
          cd ios
          xcodebuild -workspace SaayamApp.xcworkspace \
                     -scheme SaayamApp \
                     -configuration Release \
                     -destination generic/platform=iOS \
                     -archivePath $PWD/build/SaayamApp.xcarchive \
                     archive          

Advanced Workflow with Fastlane

.github/workflows/deploy.yml:

name: Deploy to Stores

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'
      
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Decode Keystore
        env:
          ENCODED_STRING: ${{ secrets.KEYSTORE_BASE64 }}
        run: |
          echo $ENCODED_STRING | base64 -d > android/app/keystore.jks          
      
      - name: Deploy to Play Store
        env:
          SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: |
          cd android
          bundle exec fastlane deploy          

  deploy-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Install CocoaPods
        run: |
          cd ios
          pod install          
      
      - name: Deploy to App Store
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
          FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
        run: |
          cd ios
          bundle exec fastlane deploy          

Fastlane Setup

Installation

# Install Fastlane
sudo gem install fastlane

# Initialize Fastlane for iOS
cd ios && fastlane init

# Initialize Fastlane for Android
cd android && fastlane init

iOS Fastfile

ios/fastlane/Fastfile:

default_platform(:ios)

platform :ios do
  desc "Build and upload to TestFlight"
  lane :beta do
    setup_ci if ENV['CI']
    
    match(
      type: "appstore",
      readonly: true
    )
    
    increment_build_number(
      xcodeproj: "SaayamApp.xcodeproj"
    )
    
    build_app(
      scheme: "SaayamApp",
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          "com.saayam.app" => "match AppStore com.saayam.app"
        }
      }
    )
    
    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )
    
    slack(
      message: "Successfully uploaded a new build to TestFlight! 🚀",
      channel: "#releases"
    )
  end

  desc "Deploy to App Store"
  lane :deploy do
    setup_ci if ENV['CI']
    
    match(
      type: "appstore",
      readonly: true
    )
    
    build_app(
      scheme: "SaayamApp",
      export_method: "app-store"
    )
    
    upload_to_app_store(
      force: true,
      reject_if_possible: true,
      skip_metadata: false,
      skip_screenshots: false,
      submit_for_review: true,
      automatic_release: false,
      submission_information: {
        add_id_info_limits_tracking: true,
        add_id_info_serves_ads: false,
        add_id_info_tracks_action: true,
        add_id_info_tracks_install: true,
        add_id_info_uses_idfa: true,
        content_rights_has_rights: true,
        content_rights_contains_third_party_content: true,
        export_compliance_platform: 'ios',
        export_compliance_compliance_required: false,
        export_compliance_encryption_updated: false,
        export_compliance_app_type: nil,
        export_compliance_uses_encryption: false,
        export_compliance_is_exempt: false,
        export_compliance_contains_third_party_cryptography: false,
        export_compliance_contains_proprietary_cryptography: false,
        export_compliance_available_on_french_store: false
      }
    )
    
    slack(
      message: "Successfully deployed to App Store! 🎉",
      channel: "#releases"
    )
  end

  desc "Create screenshots"
  lane :screenshots do
    capture_screenshots
    upload_to_app_store(skip_binary_upload: true)
  end

  error do |lane, exception|
    slack(
      message: "Error in #{lane}: #{exception.message}",
      success: false,
      channel: "#releases"
    )
  end
end

Android Fastfile

android/fastlane/Fastfile:

default_platform(:android)

platform :android do
  desc "Build and upload to Play Store Internal Testing"
  lane :beta do
    gradle(
      task: "clean bundleRelease",
      project_dir: "."
    )
    
    upload_to_play_store(
      track: 'internal',
      release_status: 'draft',
      skip_upload_metadata: true,
      skip_upload_changelogs: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
    
    slack(
      message: "Successfully uploaded a new build to Play Store Internal Testing! 🚀",
      channel: "#releases"
    )
  end

  desc "Deploy to Play Store"
  lane :deploy do
    gradle(
      task: "clean bundleRelease",
      project_dir: "."
    )
    
    upload_to_play_store(
      track: 'production',
      release_status: 'completed',
      skip_upload_metadata: false,
      skip_upload_changelogs: false,
      skip_upload_images: false,
      skip_upload_screenshots: false
    )
    
    slack(
      message: "Successfully deployed to Play Store! 🎉",
      channel: "#releases"
    )
  end

  desc "Create screenshots"
  lane :screenshots do
    capture_android_screenshots
    upload_to_play_store(skip_upload_apk: true)
  end

  error do |lane, exception|
    slack(
      message: "Error in #{lane}: #{exception.message}",
      success: false,
      channel: "#releases"
    )
  end
end

Environment Configuration

Secrets Management

GitHub Secrets to configure:

  • KEYSTORE_BASE64: Base64 encoded Android keystore
  • KEYSTORE_PASSWORD: Android keystore password
  • KEY_ALIAS: Android key alias
  • KEY_PASSWORD: Android key password
  • GOOGLE_PLAY_SERVICE_ACCOUNT: Google Play service account JSON
  • MATCH_PASSWORD: iOS certificates password
  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: Apple app-specific password
  • FASTLANE_SESSION: Fastlane session for Apple ID
  • SLACK_URL: Slack webhook URL for notifications

Match Setup (iOS)

# Initialize match
fastlane match init

# Generate certificates
fastlane match development
fastlane match appstore

Matchfile:

git_url("https://github.com/your-org/certificates")
storage_mode("git")
type("development")
app_identifier(["com.saayam.app"])
username("your-apple-id@example.com")

Branch Protection

GitHub Branch Protection Rules

# .github/branch-protection.yml
protection_rules:
  main:
    required_status_checks:
      strict: true
      contexts:
        - "test"
        - "build-android"
        - "build-ios"
    enforce_admins: true
    required_pull_request_reviews:
      required_approving_review_count: 2
      dismiss_stale_reviews: true
      require_code_owner_reviews: true
    restrictions:
      users: []
      teams: ["core-team"]

Automated Testing in CI

E2E Testing in CI

.github/workflows/e2e.yml:

name: E2E Tests

on:
  pull_request:
    branches: [main]

jobs:
  e2e-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Install CocoaPods
        run: |
          cd ios
          pod install          
      
      - name: Build for Detox
        run: detox build --configuration ios.sim.release
      
      - name: Run Detox tests
        run: detox test --configuration ios.sim.release --cleanup

  e2e-android:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'
      
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'
      
      - name: Setup Android SDK
        uses: android-actions/setup-android@v2
      
      - name: AVD cache
        uses: actions/cache@v3
        id: avd-cache
        with:
          path: |
            ~/.android/avd/*
            ~/.android/adb*            
          key: avd-29
      
      - 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: 29
          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."
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Run Detox tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 29
          script: |
            detox build --configuration android.emu.release
            detox test --configuration android.emu.release --cleanup