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.