Skip to main content
CI/CD Pipelines

Module Overview

Estimated Time: 5 hours | Difficulty: Advanced | Prerequisites: Testing, Deployment modules
Continuous Integration and Continuous Deployment (CI/CD) are essential for maintaining code quality and delivering updates efficiently. This module covers setting up automated pipelines for React Native apps using popular CI/CD platforms. What You’ll Learn:
  • GitHub Actions workflows
  • EAS Build integration
  • Automated testing pipelines
  • Code signing automation
  • Release management
  • Multi-environment deployments

CI/CD Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                    React Native CI/CD Pipeline                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Code Push          Build & Test           Deploy                          │
│   ─────────          ───────────           ──────                           │
│                                                                              │
│   ┌─────────┐       ┌─────────────┐       ┌─────────────┐                  │
│   │  Push   │──────▶│   Lint      │──────▶│  TestFlight │                  │
│   │  to PR  │       │   TypeCheck │       │  (iOS)      │                  │
│   └─────────┘       └─────────────┘       └─────────────┘                  │
│        │                   │                     │                          │
│        │            ┌─────────────┐              │                          │
│        │            │  Unit Tests │              │                          │
│        │            │  E2E Tests  │              │                          │
│        │            └─────────────┘              │                          │
│        │                   │                     │                          │
│        │            ┌─────────────┐       ┌─────────────┐                  │
│        │            │  iOS Build  │──────▶│  App Store  │                  │
│        │            │  Android    │       │  Play Store │                  │
│        │            └─────────────┘       └─────────────┘                  │
│        │                   │                     │                          │
│   ┌─────────┐       ┌─────────────┐       ┌─────────────┐                  │
│   │  Merge  │──────▶│  Release    │──────▶│  OTA Update │                  │
│   │  to Main│       │  Build      │       │  (EAS)      │                  │
│   └─────────┘       └─────────────┘       └─────────────┘                  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

GitHub Actions Setup

Basic Workflow Structure

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

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

env:
  NODE_VERSION: '18'
  EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}

jobs:
  lint-and-typecheck:
    name: Lint & TypeCheck
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Run TypeScript check
        run: npm run typecheck

  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint-and-typecheck
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

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

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          fail_ci_if_error: true

Complete CI/CD Workflow

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
  release:
    types: [published]

env:
  NODE_VERSION: '18'
  JAVA_VERSION: '17'
  RUBY_VERSION: '3.2'

jobs:
  # ============================================
  # Quality Checks
  # ============================================
  quality:
    name: Quality Checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: TypeCheck
        run: npm run typecheck

      - name: Format Check
        run: npm run format:check

  # ============================================
  # Unit Tests
  # ============================================
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test -- --coverage --ci --maxWorkers=2

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  # ============================================
  # E2E Tests (Detox)
  # ============================================
  e2e-ios:
    name: E2E Tests (iOS)
    runs-on: macos-latest
    needs: unit-tests
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Detox CLI
        run: npm install -g detox-cli

      - 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

      - name: Upload test artifacts
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: detox-artifacts-ios
          path: artifacts/

  e2e-android:
    name: E2E Tests (Android)
    runs-on: ubuntu-latest
    needs: unit-tests
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ env.JAVA_VERSION }}

      - name: Install dependencies
        run: npm ci

      - name: Install Detox CLI
        run: npm install -g detox-cli

      - name: Enable KVM
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: AVD cache
        uses: actions/cache@v3
        id: avd-cache
        with:
          path: |
            ~/.android/avd/*
            ~/.android/adb*
          key: avd-api-31

      - name: Create AVD and generate snapshot
        if: steps.avd-cache.outputs.cache-hit != 'true'
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 31
          force-avd-creation: false
          emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
          disable-animations: true
          script: echo "Generated AVD snapshot"

      - name: Build for Detox
        run: detox build --configuration android.emu.release

      - name: Run Detox tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 31
          force-avd-creation: false
          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
          disable-animations: true
          script: detox test --configuration android.emu.release --cleanup

  # ============================================
  # Build iOS
  # ============================================
  build-ios:
    name: Build iOS
    runs-on: macos-latest
    needs: [unit-tests]
    if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
          bundler-cache: true

      - name: Install dependencies
        run: npm ci

      - name: Install CocoaPods
        run: |
          cd ios
          pod install

      - name: Setup certificates
        env:
          CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
          CERTIFICATES_PASSWORD: ${{ secrets.CERTIFICATES_PASSWORD }}
          PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # Create keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security set-keychain-settings -t 3600 -u build.keychain

          # Import certificate
          echo "$CERTIFICATES_P12" | base64 --decode > certificate.p12
          security import certificate.p12 -k build.keychain -P "$CERTIFICATES_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain

          # Install provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "$PROVISIONING_PROFILE" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

      - name: Build iOS app
        run: |
          cd ios
          xcodebuild -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -configuration Release \
            -archivePath $PWD/build/MyApp.xcarchive \
            archive

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

      - name: Upload IPA artifact
        uses: actions/upload-artifact@v3
        with:
          name: ios-build
          path: ios/build/*.ipa

  # ============================================
  # Build Android
  # ============================================
  build-android:
    name: Build Android
    runs-on: ubuntu-latest
    needs: [unit-tests]
    if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ env.JAVA_VERSION }}

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

      - name: Install dependencies
        run: npm ci

      - name: Setup signing
        env:
          ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: |
          echo "$ANDROID_KEYSTORE" | base64 --decode > android/app/release.keystore
          echo "MYAPP_UPLOAD_STORE_FILE=release.keystore" >> android/gradle.properties
          echo "MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS" >> android/gradle.properties
          echo "MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD" >> android/gradle.properties
          echo "MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD" >> android/gradle.properties

      - name: Build Android AAB
        run: |
          cd android
          ./gradlew bundleRelease

      - name: Build Android APK
        run: |
          cd android
          ./gradlew assembleRelease

      - name: Upload AAB artifact
        uses: actions/upload-artifact@v3
        with:
          name: android-build-aab
          path: android/app/build/outputs/bundle/release/*.aab

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

  # ============================================
  # Deploy to TestFlight
  # ============================================
  deploy-testflight:
    name: Deploy to TestFlight
    runs-on: macos-latest
    needs: [build-ios, e2e-ios]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Download iOS build
        uses: actions/download-artifact@v3
        with:
          name: ios-build
          path: build/

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
          bundler-cache: true

      - name: Upload to TestFlight
        env:
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
        run: |
          bundle exec fastlane pilot upload \
            --ipa build/*.ipa \
            --skip_waiting_for_build_processing

  # ============================================
  # Deploy to Play Store
  # ============================================
  deploy-playstore:
    name: Deploy to Play Store
    runs-on: ubuntu-latest
    needs: [build-android, e2e-android]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Download Android build
        uses: actions/download-artifact@v3
        with:
          name: android-build-aab
          path: build/

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
          bundler-cache: true

      - name: Deploy to Play Store
        env:
          GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
        run: |
          echo "$GOOGLE_PLAY_JSON_KEY" > google-play-key.json
          bundle exec fastlane supply \
            --aab build/*.aab \
            --track internal \
            --json_key google-play-key.json

  # ============================================
  # Production Release
  # ============================================
  production-release:
    name: Production Release
    runs-on: ubuntu-latest
    needs: [deploy-testflight, deploy-playstore]
    if: github.event_name == 'release'
    steps:
      - uses: actions/checkout@v4

      - name: Promote iOS to Production
        run: |
          # Use fastlane to promote from TestFlight to App Store
          bundle exec fastlane deliver --submit_for_review

      - name: Promote Android to Production
        run: |
          # Use fastlane to promote from internal to production
          bundle exec fastlane supply --track production --track_promote_to production

EAS Build Integration

EAS Configuration

// eas.json
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "env": {
        "APP_ENV": "development"
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": false
      },
      "android": {
        "buildType": "apk"
      },
      "env": {
        "APP_ENV": "staging"
      }
    },
    "production": {
      "distribution": "store",
      "ios": {
        "resourceClass": "m-medium"
      },
      "android": {
        "buildType": "app-bundle"
      },
      "env": {
        "APP_ENV": "production"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@email.com",
        "ascAppId": "1234567890",
        "appleTeamId": "XXXXXXXXXX"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services.json",
        "track": "internal"
      }
    }
  }
}

EAS Build Workflow

# .github/workflows/eas-build.yml
name: EAS Build

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      platform:
        description: 'Platform to build'
        required: true
        default: 'all'
        type: choice
        options:
          - all
          - ios
          - android
      profile:
        description: 'Build profile'
        required: true
        default: 'preview'
        type: choice
        options:
          - development
          - preview
          - production

jobs:
  build:
    name: EAS Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Setup Expo
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test -- --ci

      - name: Build iOS
        if: github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios'
        run: eas build --platform ios --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive

      - name: Build Android
        if: github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android'
        run: eas build --platform android --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive

  submit:
    name: Submit to Stores
    runs-on: ubuntu-latest
    needs: build
    if: github.event.inputs.profile == 'production'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Expo
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Submit iOS
        run: eas submit --platform ios --latest --non-interactive

      - name: Submit Android
        run: eas submit --platform android --latest --non-interactive

Fastlane Integration

Fastfile Configuration

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Build and upload to TestFlight"
  lane :beta do
    setup_ci if ENV['CI']
    
    # Increment build number
    increment_build_number(
      build_number: ENV['GITHUB_RUN_NUMBER'] || latest_testflight_build_number + 1
    )
    
    # Build the app
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      configuration: "Release",
      export_method: "app-store"
    )
    
    # Upload to TestFlight
    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )
    
    # Notify Slack
    slack(
      message: "iOS beta build uploaded to TestFlight!",
      success: true
    )
  end

  desc "Deploy to App Store"
  lane :release do
    setup_ci if ENV['CI']
    
    # Download latest build from TestFlight
    download_dsyms
    
    # Upload to App Store
    deliver(
      submit_for_review: true,
      automatic_release: false,
      force: true,
      precheck_include_in_app_purchases: false
    )
    
    # Upload dSYMs to crash reporting
    upload_symbols_to_crashlytics
  end

  desc "Match certificates"
  lane :certificates do
    match(
      type: "appstore",
      readonly: is_ci
    )
  end
end
# android/fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Build and upload to Play Store internal track"
  lane :beta do
    # Increment version code
    increment_version_code(
      gradle_file_path: "app/build.gradle",
      version_code: ENV['GITHUB_RUN_NUMBER'].to_i
    )
    
    # Build the app
    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
    )
    
    # Notify Slack
    slack(
      message: "Android beta build uploaded to Play Store!",
      success: true
    )
  end

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

Environment Management

Environment-Specific Builds

# .github/workflows/environment-builds.yml
name: Environment Builds

on:
  push:
    branches:
      - develop    # Triggers staging build
      - main       # Triggers production build
  pull_request:
    branches: [develop, main]

jobs:
  determine-environment:
    runs-on: ubuntu-latest
    outputs:
      environment: ${{ steps.set-env.outputs.environment }}
    steps:
      - id: set-env
        run: |
          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "environment=production" >> $GITHUB_OUTPUT
          elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
            echo "environment=staging" >> $GITHUB_OUTPUT
          else
            echo "environment=development" >> $GITHUB_OUTPUT
          fi

  build:
    needs: determine-environment
    runs-on: ubuntu-latest
    environment: ${{ needs.determine-environment.outputs.environment }}
    steps:
      - uses: actions/checkout@v4

      - name: Create .env file
        run: |
          cat << EOF > .env
          API_URL=${{ vars.API_URL }}
          SENTRY_DSN=${{ secrets.SENTRY_DSN }}
          ANALYTICS_KEY=${{ secrets.ANALYTICS_KEY }}
          APP_ENV=${{ needs.determine-environment.outputs.environment }}
          EOF

      - name: Build app
        run: |
          npm ci
          npm run build:${{ needs.determine-environment.outputs.environment }}

Secrets Management

# Required GitHub Secrets:
# 
# iOS:
# - CERTIFICATES_P12: Base64 encoded .p12 file
# - CERTIFICATES_PASSWORD: Password for .p12
# - PROVISIONING_PROFILE: Base64 encoded provisioning profile
# - APP_STORE_CONNECT_API_KEY_ID: App Store Connect API Key ID
# - APP_STORE_CONNECT_API_ISSUER_ID: App Store Connect Issuer ID
# - APP_STORE_CONNECT_API_KEY: Base64 encoded .p8 key
#
# Android:
# - ANDROID_KEYSTORE: Base64 encoded keystore
# - KEYSTORE_PASSWORD: Keystore password
# - KEY_ALIAS: Key alias
# - KEY_PASSWORD: Key password
# - GOOGLE_PLAY_JSON_KEY: Service account JSON
#
# Common:
# - EXPO_TOKEN: Expo access token
# - SENTRY_DSN: Sentry DSN
# - CODECOV_TOKEN: Codecov token

Automated Version Management

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

on:
  workflow_dispatch:
    inputs:
      bump_type:
        description: 'Version bump type'
        required: true
        type: choice
        options:
          - patch
          - minor
          - major

jobs:
  bump-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

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

      - name: Configure Git
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"

      - name: Bump version
        run: |
          npm version ${{ github.event.inputs.bump_type }} -m "chore: bump version to %s"

      - name: Update native versions
        run: |
          VERSION=$(node -p "require('./package.json').version")
          
          # Update iOS
          /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" ios/MyApp/Info.plist
          
          # Update Android
          sed -i "s/versionName \".*\"/versionName \"$VERSION\"/" android/app/build.gradle

      - name: Commit and push
        run: |
          git add .
          git commit --amend --no-edit
          git push --follow-tags

Monitoring & Notifications

Slack Notifications

# Add to workflow
- name: Notify Slack on Success
  if: success()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "✅ Build Successful",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Build Successful* :white_check_mark:\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

- name: Notify Slack on Failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "❌ Build Failed",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Build Failed* :x:\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Logs>"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Best Practices

Cache Dependencies

Cache node_modules, CocoaPods, and Gradle to speed up builds

Parallel Jobs

Run independent jobs in parallel to reduce total pipeline time

Fail Fast

Run quick checks (lint, typecheck) before expensive builds

Secure Secrets

Use environment-specific secrets and rotate regularly

Next Steps

Module 41: App Store Deployment

Learn the complete process of deploying to iOS App Store and Google Play Store