Skip to main content

Module 12: Production and Deployment

CI/CD Setup

Automate building, testing, and deploying your React Native app

🎯 Learning Objectives

  • Understand CI/CD concepts for mobile development
  • Set up GitHub Actions for EAS builds
  • Configure automated testing in CI
  • Implement automated OTA updates
  • Set up automated app store submissions
  • Create a complete deployment pipeline

CI/CD Overview

CI/CD automates your development workflow, from running tests on every commit to deploying updates to production.

The CI/CD Pipeline

flowchart LR
    subgraph CI["Continuous Integration"]
        A[Push Code] --> B[Run Tests]
        B --> C[Lint & Type Check]
        C --> D[Build Check]
    end
    
    subgraph CD["Continuous Deployment"]
        D --> E{Branch?}
        E -->|main| F[Build Production]
        E -->|develop| G[Build Preview]
        F --> H[Submit to Stores]
        G --> I[Internal Distribution]
    end
    
    subgraph OTA["OTA Updates"]
        J[Merge to main] --> K[Publish Update]
        K --> L[Users Get Update]
    end
    
    style CI fill:#e3f2fd
    style CD fill:#e8f5e9
    style OTA fill:#fff3e0

CI/CD Benefits

Benefit Description
Consistency Every build follows the same process
Speed Automated builds run without manual intervention
Quality Tests run on every commit, catching bugs early
Reliability Reduces human error in deployment process
Traceability Every deployment is linked to a specific commit

Tools We'll Use

GitHub Actions

CI/CD platform integrated with GitHub

EAS Build

Cloud builds for iOS and Android

EAS Update

OTA updates for JavaScript changes

EAS Submit

Automated store submissions

GitHub Actions Setup

GitHub Actions runs workflows in response to events like pushes, pull requests, or scheduled triggers.

Create Expo Token

# 1. Go to expo.dev
# 2. Account Settings → Access Tokens
# 3. Create new token with appropriate permissions
# 4. Copy the token

# 5. Add to GitHub repository secrets
# Repository → Settings → Secrets and variables → Actions
# Add secret: EXPO_TOKEN = your-token-value

Basic Workflow Structure

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

# When to run
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

# Jobs to execute
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        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 tests
        run: npm test

Expo GitHub Action

# Use the official Expo GitHub Action
- name: Setup Expo
  uses: expo/expo-github-action@v8
  with:
    eas-version: latest
    token: ${{ secrets.EXPO_TOKEN }}

# This sets up:
# - EAS CLI
# - Authentication with your Expo account
# - Caching for faster subsequent runs

Caching for Faster Builds

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'  # Caches npm dependencies
          
      - name: Cache Expo
        uses: actions/cache@v4
        with:
          path: ~/.expo
          key: expo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

Automated Testing

Run tests automatically on every push and pull request.

Complete Test Workflow

# .github/workflows/test.yml
name: Test

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

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  typecheck:
    name: Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run typecheck

  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          
  build-check:
    name: Build Check
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Verify EAS config
        run: eas config

Pull Request Checks

# Require checks to pass before merging
# Go to: Repository Settings → Branches → Branch protection rules

# Add rule for 'main':
# ✓ Require status checks to pass
#   - lint
#   - typecheck  
#   - test
#   - build-check
# ✓ Require branches to be up to date
# ✓ Require pull request reviews

Build Workflows

Automate EAS builds based on branch or tag events.

Preview Build on Develop

# .github/workflows/preview-build.yml
name: Preview Build

on:
  push:
    branches: [develop]

jobs:
  build:
    name: Build Preview
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Build iOS Preview
        run: eas build --platform ios --profile preview --non-interactive
        
      - name: Build Android Preview
        run: eas build --platform android --profile preview --non-interactive

Production Build on Release

# .github/workflows/production-build.yml
name: Production Build

on:
  push:
    tags:
      - 'v*'  # Triggers on v1.0.0, v1.2.3, etc.

jobs:
  build:
    name: Build Production
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Build iOS
        run: eas build --platform ios --profile production --non-interactive
        
      - name: Build Android
        run: eas build --platform android --profile production --non-interactive

Build with Auto-Submit

# Build and submit to stores automatically
- name: Build and Submit iOS
  run: eas build --platform ios --profile production --auto-submit --non-interactive

- name: Build and Submit Android  
  run: eas build --platform android --profile production --auto-submit --non-interactive

Parallel Builds

# .github/workflows/build.yml
name: Build

on:
  push:
    tags: ['v*']

jobs:
  build-ios:
    name: Build iOS
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas build --platform ios --profile production --non-interactive

  build-android:
    name: Build Android
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas build --platform android --profile production --non-interactive

  # Jobs run in parallel by default!

OTA Update Workflows

Automatically publish OTA updates when JavaScript code changes.

Update on Main Branch

# .github/workflows/update.yml
name: OTA Update

on:
  push:
    branches: [main]
    paths:
      - 'app/**'
      - 'components/**'
      - 'screens/**'
      - 'services/**'
      - 'hooks/**'
      - 'utils/**'
      - 'assets/**'
      - 'package.json'

jobs:
  update:
    name: Publish Update
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Publish update
        run: eas update --branch production --message "${{ github.event.head_commit.message }}" --non-interactive

Update with Environment

# Different updates for different branches
name: OTA Update

on:
  push:
    branches: [main, develop]

jobs:
  update:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Determine branch
        id: branch
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "name=production" >> $GITHUB_OUTPUT
          else
            echo "name=preview" >> $GITHUB_OUTPUT
          fi
          
      - name: Publish update
        run: |
          eas update \
            --branch ${{ steps.branch.outputs.name }} \
            --message "${{ github.event.head_commit.message }}" \
            --non-interactive

Conditional Updates

# Only update if JS files changed (not native)
name: Smart Update

on:
  push:
    branches: [main]

jobs:
  check-changes:
    runs-on: ubuntu-latest
    outputs:
      js-changed: ${{ steps.filter.outputs.js }}
      native-changed: ${{ steps.filter.outputs.native }}
      
    steps:
      - uses: actions/checkout@v4
      
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            js:
              - 'app/**'
              - 'components/**'
              - 'hooks/**'
              - 'services/**'
            native:
              - 'app.json'
              - 'eas.json'
              - 'ios/**'
              - 'android/**'

  update:
    needs: check-changes
    if: needs.check-changes.outputs.js-changed == 'true' && needs.check-changes.outputs.native-changed == 'false'
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas update --branch production --non-interactive

  build:
    needs: check-changes
    if: needs.check-changes.outputs.native-changed == 'true'
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas build --platform all --profile production --non-interactive

Submission Workflows

Automate app store submissions after successful builds.

Setup Store Credentials

# Required GitHub Secrets:

# For iOS (App Store Connect):
# EXPO_TOKEN - Your Expo access token

# For Android (Google Play):
# GOOGLE_PLAY_SERVICE_ACCOUNT_KEY - Base64 encoded JSON key

# Encode your Google Play key:
base64 -i google-play-key.json | tr -d '\n'
# Copy output to GitHub secret

Auto-Submit Workflow

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-submit:
    name: Build and Submit
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      # Decode Google Play credentials
      - name: Setup Google Play credentials
        run: |
          echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}" | base64 -d > google-play-key.json
          
      # Build and submit iOS
      - name: Build and Submit iOS
        run: eas build --platform ios --profile production --auto-submit --non-interactive
        
      # Build and submit Android
      - name: Build and Submit Android
        run: eas build --platform android --profile production --auto-submit --non-interactive
        
      # Cleanup credentials
      - name: Cleanup
        if: always()
        run: rm -f google-play-key.json

Submit Existing Build

# .github/workflows/submit.yml
name: Submit to Stores

on:
  workflow_dispatch:
    inputs:
      ios_build_id:
        description: 'iOS Build ID (optional)'
        required: false
      android_build_id:
        description: 'Android Build ID (optional)'
        required: false

jobs:
  submit:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Setup Google Play credentials
        run: echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}" | base64 -d > google-play-key.json
          
      - name: Submit iOS
        if: inputs.ios_build_id != ''
        run: eas submit --platform ios --id ${{ inputs.ios_build_id }} --non-interactive
        
      - name: Submit Android
        if: inputs.android_build_id != ''
        run: eas submit --platform android --id ${{ inputs.android_build_id }} --non-interactive
        
      - name: Submit latest builds
        if: inputs.ios_build_id == '' && inputs.android_build_id == ''
        run: eas submit --platform all --latest --non-interactive

Complete Pipeline

A comprehensive CI/CD pipeline that handles all scenarios.

Full Pipeline Architecture

flowchart TB
    subgraph PR["Pull Request"]
        PR1[Lint] --> PR2[Type Check]
        PR2 --> PR3[Unit Tests]
        PR3 --> PR4[Build Check]
    end
    
    subgraph Develop["Develop Branch"]
        D1[All PR Checks] --> D2[Build Preview]
        D2 --> D3[Internal Distribution]
    end
    
    subgraph Main["Main Branch"]
        M1[All PR Checks] --> M2{Native Changes?}
        M2 -->|No| M3[Publish OTA Update]
        M2 -->|Yes| M4[Build Production]
    end
    
    subgraph Release["Release Tag"]
        R1[Build Production] --> R2[Submit to Stores]
        R2 --> R3[Create GitHub Release]
    end
    
    style PR fill:#e3f2fd
    style Develop fill:#fff3e0
    style Main fill:#e8f5e9
    style Release fill:#f3e5f5

Master CI/CD Workflow

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

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [main, develop]

env:
  NODE_VERSION: 20

jobs:
  # ============ CI Jobs ============
  
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  typecheck:
    name: Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run typecheck

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage --watchAll=false

  # ============ Check Changes ============
  
  check-changes:
    name: Check Changes
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')
    outputs:
      js-only: ${{ steps.filter.outputs.js == 'true' && steps.filter.outputs.native == 'false' }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            js:
              - 'app/**'
              - 'components/**'
              - 'hooks/**'
              - 'services/**'
              - 'utils/**'
            native:
              - 'app.json'
              - 'app.config.*'
              - 'eas.json'

  # ============ Preview Build (develop) ============
  
  build-preview:
    name: Build Preview
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test]
    if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas build --platform all --profile preview --non-interactive

  # ============ OTA Update (main, JS only) ============
  
  publish-update:
    name: Publish OTA Update
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test, check-changes]
    if: |
      github.ref == 'refs/heads/main' && 
      github.event_name == 'push' &&
      needs.check-changes.outputs.js-only == 'true'
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: |
          eas update \
            --branch production \
            --message "${{ github.event.head_commit.message }}" \
            --non-interactive

  # ============ Production Build (tag) ============
  
  build-production:
    name: Build Production
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test]
    if: startsWith(github.ref, 'refs/tags/v')
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Setup credentials
        run: |
          echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}" | base64 -d > google-play-key.json
          
      - name: Build and Submit
        run: |
          eas build --platform all --profile production --auto-submit --non-interactive
          
      - name: Cleanup
        if: always()
        run: rm -f google-play-key.json
        
  # ============ GitHub Release ============
  
  create-release:
    name: Create Release
    runs-on: ubuntu-latest
    needs: build-production
    if: startsWith(github.ref, 'refs/tags/v')
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          generate_release_notes: true
          draft: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Workflow Dispatch for Manual Builds

# .github/workflows/manual-build.yml
name: Manual Build

on:
  workflow_dispatch:
    inputs:
      platform:
        description: 'Platform to build'
        required: true
        type: choice
        options:
          - all
          - ios
          - android
      profile:
        description: 'Build profile'
        required: true
        type: choice
        options:
          - development
          - preview
          - production
      auto_submit:
        description: 'Auto-submit to stores'
        type: boolean
        default: false

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Setup credentials
        if: inputs.auto_submit
        run: echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}" | base64 -d > google-play-key.json
          
      - name: Build
        run: |
          eas build \
            --platform ${{ inputs.platform }} \
            --profile ${{ inputs.profile }} \
            ${{ inputs.auto_submit && '--auto-submit' || '' }} \
            --non-interactive

Slack Notifications

# Add to any job
- name: Notify Slack
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    channel: '#builds'
    fields: repo,message,commit,author,action,workflow
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Hands-On Exercises

Exercise 1: Set Up Basic CI

Create a CI workflow that runs tests on every pull request.

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

on:
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  typecheck:
    name: Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx tsc --noEmit

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --watchAll=false --passWithNoTests

# package.json scripts needed:
# "lint": "eslint . --ext .ts,.tsx",
# "typecheck": "tsc --noEmit",
# "test": "jest"

Exercise 2: Create OTA Update Workflow

Set up automatic OTA updates when pushing to main.

Show Solution
# .github/workflows/ota-update.yml
name: OTA Update

on:
  push:
    branches: [main]
    paths:
      - 'app/**'
      - 'components/**'
      - 'hooks/**'
      - 'services/**'
      - 'screens/**'
      - 'utils/**'
      - 'assets/**'

jobs:
  update:
    name: Publish Update
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          
      - name: Publish update
        run: |
          eas update \
            --branch production \
            --message "Update: ${{ github.event.head_commit.message }}" \
            --non-interactive
            
      - name: Notify team
        if: success()
        run: |
          echo "✅ OTA update published to production"
          echo "Commit: ${{ github.sha }}"
          echo "Message: ${{ github.event.head_commit.message }}"

Exercise 3: Full Release Pipeline

Create a workflow that builds and submits to stores on version tags.

Show Solution
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --watchAll=false

  build-ios:
    name: Build iOS
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas build --platform ios --profile production --auto-submit --non-interactive

  build-android:
    name: Build Android
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - name: Setup credentials
        run: echo "${{ secrets.GOOGLE_PLAY_KEY }}" | base64 -d > google-play-key.json
      - run: eas build --platform android --profile production --auto-submit --non-interactive
      - run: rm -f google-play-key.json

  release:
    name: Create Release
    needs: [build-ios, build-android]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Get version
        id: version
        run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
      - uses: softprops/action-gh-release@v1
        with:
          name: Release ${{ steps.version.outputs.version }}
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Summary

🎯 Key Takeaways

  • GitHub Actions: Native CI/CD for GitHub repositories
  • expo-github-action: Official action for EAS CLI setup
  • Automated testing: Run lint, type check, and tests on every PR
  • Build automation: Trigger builds on branch push or tags
  • OTA updates: Automatically publish updates on main branch
  • Auto-submit: Submit to stores after successful builds
  • Secrets management: Store credentials securely in GitHub

CI/CD Quick Reference

# Required GitHub Secrets
EXPO_TOKEN                      # Expo access token
GOOGLE_PLAY_SERVICE_ACCOUNT_KEY # Base64 encoded JSON

# Common workflow triggers
on:
  push:
    branches: [main]        # Push to main
    tags: ['v*']            # Version tags
  pull_request:
    branches: [main]        # PRs to main
  workflow_dispatch:        # Manual trigger

# Key EAS commands in CI
eas build --platform all --profile production --non-interactive
eas build --platform ios --profile production --auto-submit --non-interactive
eas update --branch production --message "..." --non-interactive
eas submit --platform all --latest --non-interactive

🎉 Congratulations!

You've completed Module 12: Production and Deployment! You now have all the knowledge needed to build, deploy, and maintain production React Native applications.