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 |
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:
needs: lint-and-test — đảm bảo build chỉ chạy khi lint và test pass--frozen-lockfile — đảm bảo CI dùng đúng phiên bản dependencies như localmacos-latest runner vì cần XcodeFastlane 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 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:
readonly: is_ci trên CI để tránh tạo certificate mới vô tìnhmatch --type appstoreOTA 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 |
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
| 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
Mobile apps có 2 loại version cần quản lý:
2.5.1 — hiển thị cho user trên store142 — integer tăng dần, store dùng để xác định update| 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.gradle và Info.plist, giúp tránh sai lệch version giữa các platform.
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 />;
}
| 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 |
| 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"
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.
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| Store từ chối upload | Version code/build number, bundle id/package name, signing | Tăng build number, kiểm signing/profile đúng app |
| Crash report không đọc được stack | Sourcemap upload, release name, dist/build number | Upload sourcemap cùng release id và verify trên Sentry |
| OTA làm app crash | Change có native dependency/permission/config không | Rollback OTA, full build cho native change |
| CI build chậm/flaky | Cache hit, runner, tests flaky, Pods/Gradle mismatch | Cache đú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.
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:
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:
Không dùng được khi:
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.
iOS code signing yêu cầu:
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:
readonly: true trên CI để tránh tạo cert mới vô tìnhCác chiến lược giảm build time:
jest --changedSince)Kết quả điển hình: từ 30-45 phút xuống 10-15 phút.
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:
ErrorUtils.setGlobalHandler()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).
Mobile app có 2 version numbers:
Quy tắc quan trọng:
GITHUB_RUN_NUMBER) để đảm bảo luôn tăngreact-native-version)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:
Acceptance criteria:
Yêu cầu:
@sentry/react-native vào dự ánViết checklist trước khi release: version/build number, changelog, QA device matrix, signing, sourcemap, store metadata, rollback plan.
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.
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.
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.
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.