Module 13 — CI/CD & Deployment

NÊN CÓ ⏱ 6-8 giờ 📋 Prerequisites: Module 12 — Testing

🎯 MỤC TIÊU HỌC TẬP

📖 HƯỚNG DẪN HỌC

Step 1: Đọc Fastlane Docs (1.5h)

Step 2: Setup GitHub Actions Workflow (2h)

Step 3: Đọc CodePush / EAS Docs (1.5h)

Step 4: Setup Sentry (1h)

⚡ ÔN NHANH

🧭 Vòng lặp học module này

📚 LÝ THUYẾT CHI TIẾT

1. CI/CD Pipeline cho React Native

Pipeline tổng quan

CI/CD pipeline cho RN phức tạp hơn web vì phải build native binary cho cả 2 platform. Pipeline điển hình gồm 5 giai đoạn chính:

Lint → Test → Build → Deploy → Monitor

Giai đoạn Tool Mục đích
Lint ESLint, Prettier, TypeScript Đảm bảo code quality, format đồng nhất
Test Jest, Detox, Maestro Unit test, integration test, E2E test
Build Fastlane, EAS Build, Xcode, Gradle Tạo .apk/.aab (Android) và .ipa (iOS)
Deploy Fastlane deliver, EAS Submit Upload lên TestFlight, Play Console, store
Monitor Sentry, Crashlytics Theo dõi crashes, performance, errors

Tại sao CI/CD cho mobile khó hơn web?

2. GitHub Actions cho React Native

Workflow YAML hoàn chỉnh

Dưới đây là workflow mẫu cho dự án RN với 3 jobs: lint/test, build Android, build iOS. Workflow được trigger khi push vào main hoặc tạo pull request.

name: React Native CI/CD

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

env:
  NODE_VERSION: '18'
  JAVA_VERSION: '17'

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: ${{ env.NODE_VERSION }}
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Lint
        run: yarn lint

      - name: Type check
        run: yarn tsc --noEmit

      - name: Unit tests
        run: yarn test --coverage

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

  build-android:
    needs: lint-and-test
    runs-on: ubuntu-latest
    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: 'yarn'

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

      - name: Setup Gradle cache
        uses: gradle/actions/setup-gradle@v3

      - name: Install dependencies
        run: yarn install --frozen-lockfile

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

      - name: Build Android Release
        working-directory: android
        run: ./gradlew assembleRelease
        env:
          STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
          KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
          KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}

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

  build-ios:
    needs: lint-and-test
    runs-on: macos-latest
    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: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Cache CocoaPods
        uses: actions/cache@v4
        with:
          path: ios/Pods
          key: pods-${{ hashFiles('ios/Podfile.lock') }}

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

      - name: Setup Ruby and Fastlane
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true

      - name: Build iOS with Fastlane
        run: bundle exec fastlane ios build
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}

      - name: Upload IPA artifact
        uses: actions/upload-artifact@v4
        with:
          name: ios-release
          path: "*.ipa"

Giải thích các phần quan trọng:

3. Fastlane

Fastlane là gì?

Fastlane là bộ tool tự động hóa build và release cho iOS và Android. Ba thành phần quan trọng nhất:

Cấu trúc Fastfile cho iOS:

default_platform(:ios)

platform :ios do
  before_all do
    setup_ci
  end

  lane :certificates do
    match(
      type: "appstore",
      app_identifier: "com.company.myapp",
      git_url: ENV["MATCH_GIT_URL"],
      readonly: is_ci
    )
  end

  lane :build do
    certificates
    increment_build_number(
      build_number: ENV["GITHUB_RUN_NUMBER"]
    )
    gym(
      scheme: "MyApp",
      workspace: "ios/MyApp.xcworkspace",
      export_method: "app-store",
      output_directory: "./build",
      clean: true
    )
  end

  lane :deploy do
    build
    deliver(
      submit_for_review: false,
      automatic_release: false,
      skip_metadata: true,
      skip_screenshots: true
    )
  end

  lane :beta do
    build
    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )
  end
end

Cấu trúc Fastfile cho Android:

default_platform(:android)

platform :android do
  lane :build do
    gradle(
      project_dir: "./android",
      task: "bundle",
      build_type: "Release"
    )
  end

  lane :deploy do
    build
    supply(
      track: "internal",
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
      json_key: "android/play-store-key.json",
      skip_upload_metadata: true,
      skip_upload_images: true
    )
  end

  lane :promote do
    supply(
      track: "internal",
      track_promote_to: "production",
      json_key: "android/play-store-key.json",
      skip_upload_aab: true
    )
  end
end

⚠️ match — Code Signing Management

match lưu certificates và profiles trong một private Git repo, được encrypt bằng password. Đây là cách duy nhất đáng tin cậy để quản lý code signing trên CI. Quy tắc quan trọng:

4. CodePush vs EAS Update

Over-The-Air (OTA) Update

OTA cho phép update JavaScript bundle mà không cần qua store review. Điều này cực kỳ hữu ích để hotfix bugs hoặc update UI nhanh. Tuy nhiên, OTA có giới hạn: không thể update native code, native modules, hoặc thay đổi permissions.

Tiêu chí CodePush (App Center) EAS Update
Nhà phát triển Microsoft (đã deprecated) Expo
Tương thích Bare RN, Expo (phức tạp) Expo managed & bare
Rollback Tự động rollback khi crash Manual rollback qua CLI
Branching Deployment keys (Staging/Production) Channels & branches
Tương lai Deprecated (tháng 3/2025) Được maintain tích cực
Giá Miễn phí Free tier, paid cho team
Tích hợp CI CLI-based EAS CLI, tích hợp sâu với Expo
Phân phối dần Percentage rollout Channels-based targeting

🚫 Giới hạn của OTA Updates

Ví dụ cấu hình EAS Update:

{
  "cli": {
    "version": ">=5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development"
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "channel": "production"
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "1234567890"
      },
      "android": {
        "track": "internal"
      }
    }
  }
}

Publish OTA update qua EAS CLI:

eas update --branch production --message "Fix login button crash"

eas update --branch preview --message "New onboarding flow"

eas update:rollback --branch production

5. App Distribution

Kênh phân phối trước khi lên store

Kênh Platform Giới hạn testers Yêu cầu
TestFlight iOS 10,000 (external), 100 (internal) Apple Developer Account, signed .ipa
Firebase App Distribution iOS + Android Không giới hạn Firebase project, signed binary
Google Play Internal Testing Android 100 Google Play Console, signed .aab
Ad Hoc Distribution iOS 100 devices/year Device UDIDs registered

Firebase App Distribution với Fastlane:

lane :distribute do
  build
  firebase_app_distribution(
    app: "1:123456789:android:abcdef",
    groups: "internal-testers",
    release_notes: "Build #{lane_context[SharedValues::BUILD_NUMBER]} - Bug fixes"
  )
end

6. Store Submission — Rejection Reasons

Top lý do bị Apple Reject

  1. Guideline 2.1 — Performance: App Completeness: App crash, có placeholder content, hoặc tính năng chưa hoàn thiện
  2. Guideline 4.0 — Design: UI không tuân theo Human Interface Guidelines, dùng custom UI thay cho native patterns
  3. Guideline 2.3 — Accurate Metadata: Screenshots không khớp với app thực tế, description gây hiểu lầm
  4. Guideline 5.1.1 — Data Collection and Storage: Không có Privacy Policy, thu thập data không cần thiết
  5. Guideline 3.1.1 — In-App Purchase: Dùng payment gateway bên ngoài cho digital goods, bypass Apple IAP
  6. Guideline 2.5.1 — Software Requirements: Dùng private APIs, hoặc API deprecated

Google Play Rejection Reasons phổ biến

7. Semantic Versioning cho Mobile

Versioning scheme: MAJOR.MINOR.PATCH

Mobile apps có 2 loại version cần quản lý:

Thay đổi Version bump Yêu cầu
Breaking changes, redesign lớn MAJOR (1.x → 2.0) Full store build + review
Tính năng mới MINOR (2.1 → 2.2) Full store build + review
Bug fix, hotfix PATCH (2.2.0 → 2.2.1) OTA update hoặc store build

Script tự động bump version:

{
  "scripts": {
    "version:patch": "npm version patch && npx react-native-version --never-amend",
    "version:minor": "npm version minor && npx react-native-version --never-amend",
    "version:major": "npm version major && npx react-native-version --never-amend"
  }
}

Package react-native-version tự động sync version từ package.json sang build.gradleInfo.plist, giúp tránh sai lệch version giữa các platform.

8. Feature Flags

Tại sao cần Feature Flags cho Mobile?

Khác với web, mobile không thể rollback deploy. Khi app đã lên store, user có thể không update ngay. Feature flags cho phép:

Custom feature flag implementation đơn giản:

type FeatureFlags = {
  newOnboarding: boolean;
  darkMode: boolean;
  betaPayment: boolean;
};

const DEFAULT_FLAGS: FeatureFlags = {
  newOnboarding: false,
  darkMode: false,
  betaPayment: false,
};

class FeatureFlagService {
  private flags: FeatureFlags = DEFAULT_FLAGS;

  async initialize(): Promise<void> {
    try {
      const response = await fetch(
        'https://api.myapp.com/feature-flags'
      );
      const remoteFlags = await response.json();
      this.flags = { ...DEFAULT_FLAGS, ...remoteFlags };
    } catch {
      this.flags = DEFAULT_FLAGS;
    }
  }

  isEnabled(flag: keyof FeatureFlags): boolean {
    return this.flags[flag] ?? false;
  }
}

export const featureFlags = new FeatureFlagService();

Sử dụng feature flag trong component:

function PaymentScreen() {
  const isBetaPayment = featureFlags.isEnabled('betaPayment');

  if (isBetaPayment) {
    return <NewPaymentFlow />;
  }

  return <LegacyPaymentFlow />;
}

LaunchDarkly vs Custom Implementation

Tiêu chí LaunchDarkly Custom
Setup time Nhanh (SDK có sẵn) Cần tự build backend + client
Targeting User segments, percentage rollout Tự implement
Dashboard UI đẹp, real-time Tự build hoặc dùng DB trực tiếp
Giá Đắt ($10/seat/month+) Chỉ tốn infra cost
Khi nào dùng Team lớn, cần A/B testing phức tạp Team nhỏ, chỉ cần toggle đơn giản

9. Error Monitoring

Sentry vs Firebase Crashlytics

Tiêu chí Sentry Firebase Crashlytics
Platform Cross-platform (RN, Web, Backend) Mobile only (iOS, Android, Flutter)
Error types JS errors + Native crashes + Performance Native crashes + ANRs
Source maps Tự động upload qua CLI/plugin Hỗ trợ hạn chế cho RN
Breadcrumbs Chi tiết (network, navigation, console) Cơ bản
Performance monitoring Có (transactions, spans) Riêng biệt (Firebase Performance)
Release tracking Có (issues per release) Có (crash-free users)
Giá Free tier 5K events/month Miễn phí hoàn toàn
Self-hosted Có thể Không

Setup Sentry trong React Native:

import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  tracesSampleRate: 0.2,
  environment: __DEV__ ? 'development' : 'production',
  enableAutoSessionTracking: true,
  sessionTrackingIntervalMillis: 30000,
  attachStacktrace: true,
  beforeSend(event) {
    if (__DEV__) {
      return null;
    }
    return event;
  },
});

Giải thích: tracesSampleRate: 0.2 nghĩa là chỉ trace 20% transactions để tránh overhead. beforeSend filter bỏ events trong development. enableAutoSessionTracking cho phép Sentry tính crash-free rate.

Upload source maps trong CI:

npx sentry-cli releases new "com.myapp@2.5.1+142"

npx sentry-cli releases files "com.myapp@2.5.1+142" \
  upload-sourcemaps \
  --dist 142 \
  ./build/sourcemaps

npx sentry-cli releases finalize "com.myapp@2.5.1+142"

10. Error Boundary Patterns

Error Boundary trong React Native

Error Boundary bắt JavaScript errors trong render tree, ngăn crash toàn bộ app. Kết hợp với Sentry để tự động report errors.

import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import * as Sentry from '@sentry/react-native';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  state: State = {
    hasError: false,
    error: null,
  };

  static getDerivedStateFromError(error: Error): Partial<State> {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    Sentry.withScope((scope) => {
      scope.setExtra('componentStack', errorInfo.componentStack);
      scope.setLevel('fatal');
      Sentry.captureException(error);
    });

    this.props.onError?.(error, errorInfo);
  }

  handleRetry = (): void => {
    this.setState({ hasError: false, error: null });
  };

  render(): ReactNode {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
          <Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
            Đã xảy ra lỗi
          </Text>
          <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16 }}>
            {this.state.error?.message}
          </Text>
          <TouchableOpacity
            onPress={this.handleRetry}
            style={{ backgroundColor: '#007AFF', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 }}
          >
            <Text style={{ color: '#fff', fontSize: 16 }}>Thử lại</Text>
          </TouchableOpacity>
        </View>
      );
    }

    return this.props.children;
  }
}

Sử dụng nhiều tầng Error Boundary cho granular error handling:

function App() {
  return (
    <ErrorBoundary fallback={<AppCrashScreen />}>
      <NavigationContainer>
        <ErrorBoundary fallback={<ScreenErrorFallback />}>
          <MainNavigator />
        </ErrorBoundary>
      </NavigationContainer>
    </ErrorBoundary>
  );
}

Tầng ngoài cùng bắt lỗi toàn app (navigation crash). Tầng trong bắt lỗi từng screen, cho phép user quay lại screen khác mà không crash toàn bộ app.

❓ CÂU HỎI PHỎNG VẤN + ĐÁP ÁN

🛠️ Debug playbook: release bug

Triệu chứngKiểm traFix thường gặp
Store từ chối uploadVersion code/build number, bundle id/package name, signingTăng build number, kiểm signing/profile đúng app
Crash report không đọc được stackSourcemap upload, release name, dist/build numberUpload sourcemap cùng release id và verify trên Sentry
OTA làm app crashChange có native dependency/permission/config khôngRollback OTA, full build cho native change
CI build chậm/flakyCache hit, runner, tests flaky, Pods/Gradle mismatchCache đúng key, split jobs, fix flaky test, pin tooling

Khung trả lời phỏng vấn: Mobile CI/CD khác web vì có binary, signing, store review, staged rollout, OTA boundary và crash monitoring. Một release tốt phải có rollback và quan sát sau phát hành.

1. Giải thích CI/CD pipeline cho một dự án React Native. Những challenges chính là gì?

CI/CD pipeline cho RN gồm: Lint (ESLint, TypeScript) → Test (Jest unit, Detox E2E) → Build (Android .aab + iOS .ipa) → Deploy (store upload) → Monitor (Sentry).

Challenges chính:

  • Code signing iOS: Quản lý certificates và provisioning profiles trên CI — giải quyết bằng Fastlane match
  • Build time dài: Native build mất 15-30 phút — cần caching node_modules, Pods, Gradle, build cache
  • macOS runner cho iOS: GitHub Actions macOS runner đắt gấp 10x Linux — tối ưu bằng cách chỉ build iOS khi merge vào main
  • Hai platform: Phải maintain pipeline cho cả Android và iOS, mỗi platform có tooling riêng
  • Store review: Không thể auto-deploy như web, phải chờ review 1-3 ngày
2. CodePush/OTA update hoạt động như thế nào? Khi nào nên dùng và khi nào không?

OTA update hoạt động bằng cách download JavaScript bundle mới từ server và thay thế bundle cũ trong app. Khi app khởi động, SDK kiểm tra server có update mới không, nếu có thì download và áp dụng.

Nên dùng khi:

  • Fix bug JavaScript cần push gấp (hotfix)
  • Thay đổi UI/styling, text, logic nghiệp vụ
  • A/B testing tính năng mới

Không dùng được khi:

  • Thêm/xóa native modules (camera, Bluetooth...)
  • Update React Native version
  • Thay đổi native code (Java/Kotlin, ObjC/Swift)
  • Thay đổi permissions, app icon, splash screen

Cần lưu ý: Apple cấm thay đổi "primary purpose" của app qua OTA. Luôn có rollback plan vì OTA có thể gây crash nếu JS code không tương thích với native code version hiện tại.

3. Fastlane match giải quyết vấn đề gì? Tại sao code signing trên CI lại phức tạp?

iOS code signing yêu cầu:

  • Certificate (.p12): Chứng minh danh tính developer/organization
  • Provisioning Profile: Liên kết certificate với app ID và device list

Trên máy local, Xcode quản lý tự động. Trên CI, không có Keychain hay Xcode UI → phải import certificate vào Keychain và cài profile thủ công. Nếu nhiều dev trong team, certificate dễ bị conflict (mỗi người tạo cert riêng, Apple giới hạn 3 distribution certificates).

match giải quyết bằng cách:

  • Lưu trữ tất cả certs/profiles trong 1 private Git repo, encrypt bằng password
  • Team share cùng 1 bộ cert/profile thay vì mỗi người tạo riêng
  • CI clone repo, decrypt và import vào Keychain tự động
  • Dùng readonly: true trên CI để tránh tạo cert mới vô tình
4. Làm thế nào để giảm build time trên CI cho React Native?

Các chiến lược giảm build time:

  • Caching dependencies: Cache node_modules (dựa vào yarn.lock hash), CocoaPods (Podfile.lock hash), Gradle wrapper + dependencies
  • Skip unnecessary builds: Chỉ build khi merge vào main/release branch, PR chỉ chạy lint + test
  • Parallel jobs: Chạy Android build và iOS build song song, lint/test trên job riêng
  • Incremental builds: Bật Gradle build cache, sử dụng ccache cho Xcode
  • Selective testing: Chỉ chạy test cho files thay đổi trên PR (jest --changedSince)
  • Pre-built dependencies: Dùng binary Pods thay vì build from source
  • Dedicated runners: Self-hosted macOS runner nhanh hơn GitHub-hosted

Kết quả điển hình: từ 30-45 phút xuống 10-15 phút.

5. Error Boundary trong React Native khác gì so với React Web? Giới hạn là gì?

Error Boundary trong RN hoạt động giống React Web — chỉ bắt được lỗi trong render phase và lifecycle methods. Điểm khác biệt và giới hạn:

  • Native crashes: Error Boundary KHÔNG bắt được native crash (null pointer trong Java/ObjC), cần Sentry/Crashlytics
  • Async errors: Không bắt lỗi trong setTimeout, Promise, event handlers — cần global error handler: ErrorUtils.setGlobalHandler()
  • Navigation: Error Boundary phải wrap đúng level — nếu wrap quá cao, crash một screen sẽ mất toàn bộ navigation state
  • Recovery: Trên mobile, fallback UI cần có nút "Thử lại" vì user không thể refresh page như web

Best practice: dùng nhiều tầng Error Boundary — tầng app-level (last resort), tầng screen-level (cho phép navigate sang screen khác), tầng component-level (cho widgets có thể fail independently).

6. Giải thích semantic versioning cho mobile app. Version name và version code khác nhau thế nào?

Mobile app có 2 version numbers:

  • Version Name (versionName / CFBundleShortVersionString): Format MAJOR.MINOR.PATCH (ví dụ 2.5.1). Hiển thị cho user trên store. Tuân theo semver: MAJOR khi breaking change, MINOR khi thêm feature, PATCH khi fix bug.
  • Version Code (versionCode / CFBundleVersion): Integer tăng dần (ví dụ 142). Store dùng để xác định binary mới hơn. PHẢI tăng mỗi lần upload, không bao giờ giảm.

Quy tắc quan trọng:

  • Có thể giữ nguyên version name và chỉ tăng version code (khi fix build issue)
  • Dùng CI build number làm version code (GITHUB_RUN_NUMBER) để đảm bảo luôn tăng
  • Sync version giữa package.json, build.gradle, Info.plist bằng tool (react-native-version)
  • OTA update không thay đổi version name/code — chỉ thay đổi JS bundle

🏋️ BÀI TẬP

Bài tập: Viết GitHub Actions Workflow cho React Native

Yêu cầu: Tạo một GitHub Actions workflow hoàn chỉnh cho dự án React Native với các yêu cầu:

  1. Job 1 — Quality Check (chạy trên mọi PR):
    • Checkout code
    • Setup Node.js 18 với yarn cache
    • Install dependencies với frozen lockfile
    • Chạy ESLint
    • Chạy TypeScript type check
    • Chạy Jest tests với coverage
    • Upload coverage report
  2. Job 2 — Build Android (chỉ khi push vào main):
    • Depends on Job 1
    • Setup Java 17
    • Decode keystore từ secrets
    • Build release APK
    • Upload APK artifact
  3. Job 3 — Build iOS (chỉ khi push vào main):
    • Depends on Job 1
    • Chạy trên macOS runner
    • Cache CocoaPods
    • Build với Fastlane
    • Upload IPA artifact
  4. Job 4 — Deploy to TestFlight/Play Store (chỉ khi tag release):
    • Depends on Job 2 & 3
    • Download artifacts
    • Upload iOS lên TestFlight qua Fastlane
    • Upload Android lên Play Store internal track
    • Gửi Slack notification

Acceptance criteria:

Bài tập bổ sung: Setup Sentry Error Monitoring

Yêu cầu:

  1. Tích hợp @sentry/react-native vào dự án
  2. Tạo Error Boundary component với Sentry reporting
  3. Implement 3 tầng Error Boundary (app → screen → widget)
  4. Thêm custom breadcrumbs cho navigation events
  5. Viết script upload source maps trong CI

🧩 MINI EXERCISE THỰC TẾ REACT NATIVE

Exercise 1: Release checklist

Viết checklist trước khi release: version/build number, changelog, QA device matrix, signing, sourcemap, store metadata, rollback plan.

Exercise 2: OTA decision drill

Cho 5 thay đổi: sửa text, thêm native package, đổi API URL, thêm permission camera, fix crash JS. Phân loại cái nào OTA được, cái nào phải full build, giải thích lý do.

Exercise 3: CI failure playbook

Tạo bảng xử lý lỗi: iOS signing fail, Gradle cache lỗi, CocoaPods mismatch, test flaky, sourcemap upload fail. Mỗi lỗi có symptom, nguyên nhân khả dĩ, bước debug đầu tiên.

⚠️ LỖI THƯỜNG GẶP

🏢 CASE ĐI LÀM THẬT

Case: Team publish OTA để fix crash, nhưng bản fix import native package mới. User đang dùng binary cũ nhận JS mới và crash ngay khi mở app.

Cách xử lý: Chỉ OTA khi thay đổi nằm trong JS/assets tương thích binary hiện tại. Với native dependency, permission, Info.plist/AndroidManifest, build.gradle/Podfile: tạo full build và rollout staged.

🧠 GHI NHỚ NHANH

CI/CD mobile là quản lý rủi ro release, không chỉ chạy workflow. Một release tốt có signed build, version đúng, sourcemap đúng, rollback rõ, và monitoring sau khi phát hành.

❔ CÂU HỎI TỰ KIỂM TRA

1. Vì sao OTA không thay thế được full build?
2. Build number khác version name thế nào?
3. Khi iOS signing fail trên CI, bạn kiểm tra gì đầu tiên?
4. Sau release, metric nào cần xem trong 1-2 giờ đầu?

✅ CHECKLIST TỰ ĐÁNH GIÁ

📎 TÀI NGUYÊN

Documentation

Articles

Video

Tools