581 lines
16 KiB
Markdown
581 lines
16 KiB
Markdown
# OTA Updates with CodePush
|
|
|
|
## CodePush Setup
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
# Install CodePush CLI
|
|
npm install -g code-push-cli
|
|
|
|
# Install React Native CodePush
|
|
yarn add react-native-code-push
|
|
```
|
|
|
|
### App Center Configuration
|
|
|
|
```bash
|
|
# Login to App Center
|
|
code-push login
|
|
|
|
# Create apps
|
|
code-push app add SaayamApp-iOS ios react-native
|
|
code-push app add SaayamApp-Android android react-native
|
|
|
|
# Get deployment keys
|
|
code-push deployment ls SaayamApp-iOS -k
|
|
code-push deployment ls SaayamApp-Android -k
|
|
```
|
|
|
|
### Environment Configuration
|
|
|
|
**.env files:**
|
|
|
|
```bash
|
|
# .env.staging
|
|
CODEPUSH_DEPLOYMENT_KEY_IOS=staging-ios-key
|
|
CODEPUSH_DEPLOYMENT_KEY_ANDROID=staging-android-key
|
|
|
|
# .env.production
|
|
CODEPUSH_DEPLOYMENT_KEY_IOS=production-ios-key
|
|
CODEPUSH_DEPLOYMENT_KEY_ANDROID=production-android-key
|
|
```
|
|
|
|
## CodePush Integration
|
|
|
|
### App Configuration
|
|
|
|
**App.tsx:**
|
|
|
|
```typescript
|
|
import React, { useEffect, useState } from "react";
|
|
import { Alert, Platform } from "react-native";
|
|
import codePush from "react-native-code-push";
|
|
import Config from "react-native-config";
|
|
import { AppNavigator } from "./src/navigation/AppNavigator";
|
|
import { LoadingScreen } from "./src/components/LoadingScreen";
|
|
|
|
const codePushOptions = {
|
|
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
|
|
installMode: codePush.InstallMode.ON_NEXT_RESUME,
|
|
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
|
|
deploymentKey: Platform.select({
|
|
ios: Config.CODEPUSH_DEPLOYMENT_KEY_IOS,
|
|
android: Config.CODEPUSH_DEPLOYMENT_KEY_ANDROID,
|
|
}),
|
|
updateDialog: {
|
|
appendReleaseDescription: true,
|
|
descriptionPrefix: "\n\nWhat's New:\n",
|
|
mandatoryContinueButtonLabel: "Install Now",
|
|
mandatoryUpdateMessage:
|
|
"A critical update is available and must be installed.",
|
|
optionalIgnoreButtonLabel: "Later",
|
|
optionalInstallButtonLabel: "Install",
|
|
optionalUpdateMessage:
|
|
"An update is available. Would you like to install it?",
|
|
title: "Update Available",
|
|
},
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
const [syncInProgress, setSyncInProgress] = useState(false);
|
|
const [syncMessage, setSyncMessage] = useState("");
|
|
|
|
useEffect(() => {
|
|
codePush.notifyAppReady();
|
|
}, []);
|
|
|
|
const codePushStatusDidChange = (syncStatus: codePush.SyncStatus) => {
|
|
switch (syncStatus) {
|
|
case codePush.SyncStatus.CHECKING_FOR_UPDATE:
|
|
setSyncMessage("Checking for updates...");
|
|
break;
|
|
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
|
|
setSyncMessage("Downloading update...");
|
|
setSyncInProgress(true);
|
|
break;
|
|
case codePush.SyncStatus.INSTALLING_UPDATE:
|
|
setSyncMessage("Installing update...");
|
|
break;
|
|
case codePush.SyncStatus.UP_TO_DATE:
|
|
setSyncMessage("App is up to date");
|
|
setSyncInProgress(false);
|
|
break;
|
|
case codePush.SyncStatus.UPDATE_INSTALLED:
|
|
setSyncMessage("Update installed successfully");
|
|
setSyncInProgress(false);
|
|
break;
|
|
default:
|
|
setSyncInProgress(false);
|
|
break;
|
|
}
|
|
};
|
|
|
|
const codePushDownloadDidProgress = (progress: codePush.DownloadProgress) => {
|
|
const percentage = Math.round(
|
|
(progress.receivedBytes / progress.totalBytes) * 100
|
|
);
|
|
setSyncMessage(`Downloading update... ${percentage}%`);
|
|
};
|
|
|
|
if (syncInProgress) {
|
|
return <LoadingScreen message={syncMessage} />;
|
|
}
|
|
|
|
return <AppNavigator />;
|
|
};
|
|
|
|
export default codePush(codePushOptions)(App);
|
|
```
|
|
|
|
### Manual Update Check
|
|
|
|
```typescript
|
|
// src/utils/updateManager.ts
|
|
import codePush from "react-native-code-push";
|
|
import { Alert } from "react-native";
|
|
|
|
export class UpdateManager {
|
|
static async checkForUpdate(): Promise<void> {
|
|
try {
|
|
const update = await codePush.checkForUpdate();
|
|
|
|
if (update) {
|
|
Alert.alert(
|
|
"Update Available",
|
|
`Version ${update.appVersion} is available. Would you like to install it now?`,
|
|
[
|
|
{ text: "Later", style: "cancel" },
|
|
{ text: "Install", onPress: () => this.downloadAndInstall() },
|
|
]
|
|
);
|
|
} else {
|
|
Alert.alert("No Updates", "Your app is up to date!");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking for updates:", error);
|
|
}
|
|
}
|
|
|
|
static async downloadAndInstall(): Promise<void> {
|
|
try {
|
|
await codePush.sync({
|
|
installMode: codePush.InstallMode.IMMEDIATE,
|
|
updateDialog: {
|
|
appendReleaseDescription: true,
|
|
descriptionPrefix: "\n\nChanges:\n",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Error downloading update:", error);
|
|
Alert.alert(
|
|
"Update Failed",
|
|
"Failed to download update. Please try again later."
|
|
);
|
|
}
|
|
}
|
|
|
|
static async getUpdateMetadata(): Promise<codePush.LocalPackage | null> {
|
|
try {
|
|
return await codePush.getUpdateMetadata();
|
|
} catch (error) {
|
|
console.error("Error getting update metadata:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async rollback(): Promise<void> {
|
|
try {
|
|
await codePush.restartApp();
|
|
} catch (error) {
|
|
console.error("Error rolling back:", error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Deployment Strategies
|
|
|
|
### Staged Rollout
|
|
|
|
```bash
|
|
# Deploy to staging first
|
|
code-push release-react SaayamApp-iOS ios --deploymentName Staging
|
|
|
|
# Test staging deployment
|
|
# If successful, promote to production with rollout percentage
|
|
|
|
# Deploy to 10% of production users
|
|
code-push release-react SaayamApp-iOS ios --deploymentName Production --rollout 10%
|
|
|
|
# Monitor metrics and gradually increase rollout
|
|
code-push patch SaayamApp-iOS Production --rollout 25%
|
|
code-push patch SaayamApp-iOS Production --rollout 50%
|
|
code-push patch SaayamApp-iOS Production --rollout 100%
|
|
```
|
|
|
|
### Automated Deployment
|
|
|
|
**scripts/deploy-codepush.js:**
|
|
|
|
```javascript
|
|
#!/usr/bin/env node
|
|
|
|
const { execSync } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const platform = process.argv[2]; // ios or android
|
|
const environment = process.argv[3] || "staging"; // staging or production
|
|
const rollout = process.argv[4] || "100"; // rollout percentage
|
|
|
|
if (!platform || !["ios", "android"].includes(platform)) {
|
|
console.error(
|
|
"Usage: node deploy-codepush.js <ios|android> [staging|production] [rollout%]"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const appName = platform === "ios" ? "SaayamApp-iOS" : "SaayamApp-Android";
|
|
const deploymentName = environment === "production" ? "Production" : "Staging";
|
|
|
|
// Get version from package.json
|
|
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
const version = packageJson.version;
|
|
|
|
// Generate release notes from git commits
|
|
const releaseNotes = execSync("git log --oneline -10", { encoding: "utf8" })
|
|
.split("\n")
|
|
.filter((line) => line.trim())
|
|
.map((line) => `• ${line.split(" ").slice(1).join(" ")}`)
|
|
.join("\n");
|
|
|
|
console.log(`Deploying to ${appName} ${deploymentName}...`);
|
|
console.log(`Version: ${version}`);
|
|
console.log(`Rollout: ${rollout}%`);
|
|
|
|
try {
|
|
const command = [
|
|
"code-push release-react",
|
|
appName,
|
|
platform,
|
|
`--deploymentName ${deploymentName}`,
|
|
`--description "${releaseNotes}"`,
|
|
`--rollout ${rollout}%`,
|
|
"--mandatory false",
|
|
].join(" ");
|
|
|
|
execSync(command, { stdio: "inherit" });
|
|
|
|
console.log(`✅ Successfully deployed to ${appName} ${deploymentName}`);
|
|
|
|
// Send notification (Slack, email, etc.)
|
|
notifyDeployment(appName, deploymentName, version, rollout);
|
|
} catch (error) {
|
|
console.error("❌ Deployment failed:", error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
function notifyDeployment(appName, deployment, version, rollout) {
|
|
// Implementation for sending notifications
|
|
console.log(
|
|
`📱 ${appName} ${deployment} v${version} deployed to ${rollout}% of users`
|
|
);
|
|
}
|
|
```
|
|
|
|
### Rollback Strategy
|
|
|
|
```bash
|
|
# Check deployment history
|
|
code-push deployment history SaayamApp-iOS Production
|
|
|
|
# Rollback to previous version
|
|
code-push rollback SaayamApp-iOS Production
|
|
|
|
# Rollback to specific label
|
|
code-push rollback SaayamApp-iOS Production --targetRelease v1.2.3
|
|
```
|
|
|
|
## Update Policies
|
|
|
|
### Update Configuration
|
|
|
|
```typescript
|
|
// src/config/updatePolicy.ts
|
|
import codePush from "react-native-code-push";
|
|
|
|
export const UpdatePolicy = {
|
|
// Check for updates when app resumes from background
|
|
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
|
|
|
|
// Install updates on next app restart (non-disruptive)
|
|
installMode: codePush.InstallMode.ON_NEXT_RESTART,
|
|
|
|
// For mandatory updates, install immediately
|
|
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
|
|
|
|
// Minimum background duration before checking (in seconds)
|
|
minimumBackgroundDuration: 60,
|
|
|
|
// Update dialog configuration
|
|
updateDialog: {
|
|
appendReleaseDescription: true,
|
|
descriptionPrefix: "\n\nWhat's New:\n",
|
|
mandatoryContinueButtonLabel: "Install Now",
|
|
mandatoryUpdateMessage:
|
|
"A critical update is required to continue using the app.",
|
|
optionalIgnoreButtonLabel: "Not Now",
|
|
optionalInstallButtonLabel: "Install",
|
|
optionalUpdateMessage:
|
|
"An update is available with new features and improvements.",
|
|
title: "Update Available",
|
|
},
|
|
};
|
|
```
|
|
|
|
### Conditional Updates
|
|
|
|
```typescript
|
|
// src/utils/conditionalUpdates.ts
|
|
import codePush from "react-native-code-push";
|
|
import { Platform } from "react-native";
|
|
import DeviceInfo from "react-native-device-info";
|
|
|
|
export class ConditionalUpdates {
|
|
static async shouldCheckForUpdate(): Promise<boolean> {
|
|
// Don't check for updates in development
|
|
if (__DEV__) return false;
|
|
|
|
// Check device conditions
|
|
const batteryLevel = await DeviceInfo.getBatteryLevel();
|
|
const isCharging = await DeviceInfo.isBatteryCharging();
|
|
const connectionType = await DeviceInfo.getConnectionType();
|
|
|
|
// Only update if:
|
|
// - Battery > 20% OR device is charging
|
|
// - Connected to WiFi (for large updates)
|
|
const batteryOk = batteryLevel > 0.2 || isCharging;
|
|
const connectionOk = connectionType === "wifi";
|
|
|
|
return batteryOk && connectionOk;
|
|
}
|
|
|
|
static async checkForUpdateWithConditions(): Promise<void> {
|
|
const shouldCheck = await this.shouldCheckForUpdate();
|
|
|
|
if (!shouldCheck) {
|
|
console.log("Skipping update check due to device conditions");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const update = await codePush.checkForUpdate();
|
|
|
|
if (update) {
|
|
// Check update size
|
|
const updateSizeMB = (update.packageSize || 0) / (1024 * 1024);
|
|
|
|
if (updateSizeMB > 10) {
|
|
// Large update - require WiFi and user confirmation
|
|
this.promptForLargeUpdate(update, updateSizeMB);
|
|
} else {
|
|
// Small update - download automatically
|
|
this.downloadUpdate(update);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking for conditional update:", error);
|
|
}
|
|
}
|
|
|
|
private static promptForLargeUpdate(
|
|
update: codePush.RemotePackage,
|
|
sizeMB: number
|
|
) {
|
|
// Show custom dialog for large updates
|
|
// Implementation depends on your UI framework
|
|
}
|
|
|
|
private static async downloadUpdate(update: codePush.RemotePackage) {
|
|
try {
|
|
await update.download();
|
|
await update.install(codePush.InstallMode.ON_NEXT_RESTART);
|
|
} catch (error) {
|
|
console.error("Error downloading update:", error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Monitoring & Analytics
|
|
|
|
### Update Analytics
|
|
|
|
```typescript
|
|
// src/utils/updateAnalytics.ts
|
|
import AnalyticsService from "@config/analytics.config";
|
|
import codePush from "react-native-code-push";
|
|
|
|
export class UpdateAnalytics {
|
|
static trackUpdateCheck() {
|
|
AnalyticsService.logEvent("codepush_check_for_update");
|
|
}
|
|
|
|
static trackUpdateAvailable(update: codePush.RemotePackage) {
|
|
AnalyticsService.logEvent("codepush_update_available", {
|
|
app_version: update.appVersion,
|
|
package_size: update.packageSize,
|
|
is_mandatory: update.isMandatory,
|
|
deployment_key: update.deploymentKey,
|
|
});
|
|
}
|
|
|
|
static trackUpdateDownloadStart(update: codePush.RemotePackage) {
|
|
AnalyticsService.logEvent("codepush_download_start", {
|
|
app_version: update.appVersion,
|
|
package_size: update.packageSize,
|
|
});
|
|
}
|
|
|
|
static trackUpdateDownloadComplete(update: codePush.LocalPackage) {
|
|
AnalyticsService.logEvent("codepush_download_complete", {
|
|
app_version: update.appVersion,
|
|
package_size: update.packageSize,
|
|
});
|
|
}
|
|
|
|
static trackUpdateInstalled(update: codePush.LocalPackage) {
|
|
AnalyticsService.logEvent("codepush_update_installed", {
|
|
app_version: update.appVersion,
|
|
install_mode: update.installMode,
|
|
});
|
|
}
|
|
|
|
static trackUpdateFailed(error: Error, context?: string) {
|
|
AnalyticsService.logEvent("codepush_update_failed", {
|
|
error_message: error.message,
|
|
context: context || "unknown",
|
|
});
|
|
}
|
|
|
|
static trackRollback(reason: string) {
|
|
AnalyticsService.logEvent("codepush_rollback", {
|
|
reason,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Health Monitoring
|
|
|
|
```typescript
|
|
// src/utils/updateHealthMonitor.ts
|
|
import codePush from "react-native-code-push";
|
|
import CrashlyticsService from "@config/firebase.config";
|
|
|
|
export class UpdateHealthMonitor {
|
|
private static readonly CRASH_THRESHOLD = 3;
|
|
private static crashCount = 0;
|
|
|
|
static async monitorUpdateHealth() {
|
|
try {
|
|
const updateMetadata = await codePush.getUpdateMetadata();
|
|
|
|
if (updateMetadata && updateMetadata.isFirstRun) {
|
|
// This is the first run after an update
|
|
this.trackFirstRunAfterUpdate(updateMetadata);
|
|
|
|
// Start monitoring for crashes
|
|
this.startCrashMonitoring();
|
|
}
|
|
} catch (error) {
|
|
console.error("Error monitoring update health:", error);
|
|
}
|
|
}
|
|
|
|
private static trackFirstRunAfterUpdate(update: codePush.LocalPackage) {
|
|
CrashlyticsService.log(
|
|
`First run after CodePush update: ${update.appVersion}`
|
|
);
|
|
|
|
// Set custom attributes for crash reporting
|
|
CrashlyticsService.setUserAttributes({
|
|
codepush_version: update.appVersion || "unknown",
|
|
codepush_label: update.label || "unknown",
|
|
is_codepush_update: "true",
|
|
});
|
|
}
|
|
|
|
private static startCrashMonitoring() {
|
|
// Monitor app crashes for the next 24 hours
|
|
const monitoringDuration = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
setTimeout(() => {
|
|
this.evaluateUpdateStability();
|
|
}, monitoringDuration);
|
|
}
|
|
|
|
private static async evaluateUpdateStability() {
|
|
if (this.crashCount >= this.CRASH_THRESHOLD) {
|
|
console.warn(
|
|
`Update appears unstable (${this.crashCount} crashes). Consider rollback.`
|
|
);
|
|
|
|
// Optionally trigger automatic rollback
|
|
// await this.performAutomaticRollback();
|
|
} else {
|
|
console.log("Update appears stable");
|
|
CrashlyticsService.log("CodePush update validated as stable");
|
|
}
|
|
}
|
|
|
|
private static async performAutomaticRollback() {
|
|
try {
|
|
await codePush.restartApp();
|
|
CrashlyticsService.log("Automatic rollback performed due to instability");
|
|
} catch (error) {
|
|
console.error("Failed to perform automatic rollback:", error);
|
|
}
|
|
}
|
|
|
|
static reportCrash() {
|
|
this.crashCount++;
|
|
CrashlyticsService.log(`Crash reported. Count: ${this.crashCount}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Update Guidelines
|
|
|
|
1. **Test Thoroughly**
|
|
|
|
- Test updates on staging environment
|
|
- Verify on multiple devices and OS versions
|
|
- Test offline scenarios
|
|
|
|
2. **Gradual Rollout**
|
|
|
|
- Start with small percentage (5-10%)
|
|
- Monitor crash rates and user feedback
|
|
- Gradually increase rollout
|
|
|
|
3. **Rollback Strategy**
|
|
|
|
- Have rollback plan ready
|
|
- Monitor key metrics after deployment
|
|
- Set up automated alerts for issues
|
|
|
|
4. **Update Frequency**
|
|
|
|
- Don't update too frequently (user fatigue)
|
|
- Bundle related changes together
|
|
- Consider user timezone for deployments
|
|
|
|
5. **Communication**
|
|
- Provide clear release notes
|
|
- Notify users of important changes
|
|
- Use appropriate update messaging
|