CI/CD & Release Management Strategy

📖 Concept

CI/CD for React Native is more complex than web because you're building for two platforms (iOS + Android), managing native dependencies, and dealing with app store review processes.

CI Pipeline stages:

PR Created
  → Lint (TypeScript, ESLint, Prettier check)
  → Unit Tests (Jest, ~2 min)
  → Integration Tests (RNTL + MSW, ~5 min)
  → Build Check (Metro bundling, ~3 min)
  → [Optional] E2E Tests on PR (Detox, ~15 min)

Merge to main
  → All above
  → E2E Tests (full suite, ~30 min)
  → Build iOS (Xcode, ~15 min)
  → Build Android (Gradle, ~15 min)
  → Upload to TestFlight / Firebase App Distribution

Release Branch
  → Full test suite
  → Code signing
  → Store submission (App Store Connect, Google Play Console)
  → [OTA] CodePush for JS-only changes

Release strategies:

  1. Full store release: Native + JS changes. 1-7 days review time. Staged rollout (1% → 10% → 100%).
  2. OTA update (CodePush): JS-only changes. Instant delivery. No store review. Use for bug fixes, content changes.
  3. Feature flags: Ship code behind flags. Enable/disable without any release. Gradual rollout.

Monitoring & observability post-release:

  • Crash-free rate (Sentry, Crashlytics) — target >99.5%
  • ANR rate (Android vitals) — target <0.5%
  • App startup time — monitor p50, p95, p99
  • Bundle size tracking — alert on >5% increase
  • API error rates — dashboard per endpoint
  • User-reported issues — in-app feedback, support tickets

💻 Code Example

codeTap to expand ⛶
1// === CI/CD CONFIGURATION ===
2
3// .github/workflows/ci.yml (GitHub Actions)
4const ciConfig = {
5 name: 'CI',
6 on: {
7 pull_request: { branches: ['main', 'develop'] },
8 push: { branches: ['main'] },
9 },
10 jobs: {
11 lint_and_test: {
12 'runs-on': 'ubuntu-latest',
13 steps: [
14 { uses: 'actions/checkout@v4' },
15 { uses: 'actions/setup-node@v4', with: { 'node-version': '20' } },
16 { run: 'npm ci' },
17 { run: 'npm run lint', name: 'ESLint' },
18 { run: 'npm run typecheck', name: 'TypeScript' },
19 { run: 'npm test -- --coverage', name: 'Unit Tests' },
20 {
21 name: 'Coverage Gate',
22 run: 'npx coverage-threshold --statements 80 --branches 70',
23 },
24 ],
25 },
26 build_android: {
27 'runs-on': 'ubuntu-latest',
28 needs: 'lint_and_test',
29 if: "github.event_name == 'push'",
30 steps: [
31 // Setup Java, NDK, build, upload to Firebase App Distribution
32 ],
33 },
34 build_ios: {
35 'runs-on': 'macos-latest',
36 needs: 'lint_and_test',
37 if: "github.event_name == 'push'",
38 steps: [
39 // Setup Xcode, fastlane, build, upload to TestFlight
40 ],
41 },
42 },
43};
44
45// === RELEASE MANAGEMENT ===
46
47// Version management strategy
48// package.json version = marketing version (1.2.3)
49// Build number auto-incremented by CI
50
51// Release checklist (automated where possible)
52const releaseChecklist = {
53 preRelease: [
54 'All tests passing on release branch',
55 'No P0/P1 bugs open',
56 'Release notes written',
57 'Staging build tested by QA',
58 'Performance benchmarks within thresholds',
59 'Bundle size within budget',
60 'Crash-free rate on staging > 99.5%',
61 ],
62 release: [
63 'Tag release in git (v1.2.3)',
64 'Build signed APK/IPA',
65 'Submit to App Store Connect',
66 'Submit to Google Play Console',
67 'Staged rollout: 1% for 24h',
68 'Monitor crash rates, ANR rates, support tickets',
69 'If metrics ok: 10% for 24h → 50% → 100%',
70 ],
71 postRelease: [
72 'Verify analytics events flowing',
73 'Monitor error rates for 48h',
74 'Close release-related tickets',
75 'Update internal documentation',
76 'Start next release branch',
77 ],
78};
79
80// OTA updates with CodePush
81import codePush from 'react-native-code-push';
82
83const codePushOptions = {
84 checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
85 installMode: codePush.InstallMode.ON_NEXT_RESTART,
86 // Don't force immediate install — user might be mid-action
87};
88
89// Wrap root component
90const App = codePush(codePushOptions)(AppRoot);
91
92// For critical fixes — install immediately
93async function deployHotfix() {
94 const update = await codePush.checkForUpdate();
95 if (update && update.isMandatory) {
96 await update.download();
97 update.install(codePush.InstallMode.IMMEDIATE);
98 // Shows update dialog and restarts app
99 }
100}

🏋️ Practice Exercise

CI/CD & Release Exercises:

  1. Set up a GitHub Actions CI pipeline that runs lint, typecheck, and tests on every PR
  2. Configure Fastlane for automated iOS TestFlight deployment
  3. Set up CodePush and deploy a JS-only update to a staging environment
  4. Create a release checklist that includes bundle size comparison with the previous release
  5. Implement a staged rollout strategy with automated rollback on crash rate increase
  6. Set up monitoring dashboards for crash rate, startup time, and API error rates

⚠️ Common Mistakes

  • Not caching node_modules and native dependencies in CI — builds take 20+ minutes instead of 5

  • Using CodePush for changes that include native code — native changes require a full store release

  • Not testing on real devices in CI — simulators miss device-specific bugs (camera, GPS, Bluetooth)

  • Skipping staged rollout — deploying to 100% immediately means a bug affects all users instantly

  • Not tracking bundle size over time — gradual increases go unnoticed until the app exceeds size limits

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for CI/CD & Release Management Strategy. Login to unlock this feature.