Release Management
Overview
Release management for mobile applications involves coordinating app store deployments, managing release cycles, automating distribution pipelines, and ensuring smooth rollouts across Android and iOS platforms. This documentation covers comprehensive release management strategies with CI/CD integration and automated testing.
Release Pipeline Architecture
CI/CD Pipeline Configuration
GitHub Actions Workflow
yaml
# .github/workflows/mobile-release.yml
name: Mobile Release Pipeline
on:
push:
branches: [main, release/*]
tags: ['v*']
pull_request:
branches: [main]
env:
NODE_VERSION: '18'
JAVA_VERSION: '11'
RUBY_VERSION: '3.0'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Run linting
run: npm run lint
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: |
coverage/
test-results.xml
build-android:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Decode keystore
run: |
echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d > android/app/release.keystore
- name: Build Android Bundle
run: |
cd android
./gradlew bundleRelease
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Upload Android Bundle
uses: actions/upload-artifact@v3
with:
name: android-bundle
path: android/app/build/outputs/bundle/release/app-release.aab
- name: Upload to Play Console
if: startsWith(github.ref, 'refs/tags/v')
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.yourcompany.yourapp
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
track: internal
status: completed
build-ios:
needs: test
runs-on: macos-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.RUBY_VERSION }}
bundler-cache: true
- name: Install dependencies
run: npm ci
- name: Install CocoaPods dependencies
run: |
cd ios
pod install --repo-update
- name: Setup certificates and provisioning profiles
run: |
echo ${{ secrets.IOS_CERTIFICATE_BASE64 }} | base64 -d > ios/certificate.p12
echo ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }} | base64 -d > ios/profile.mobileprovision
security create-keychain -p "" build.keychain
security import ios/certificate.p12 -k build.keychain -P ${{ secrets.IOS_CERTIFICATE_PASSWORD }} -A
security list-keychains -s build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp ios/profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build iOS Archive
run: |
cd ios
xcodebuild clean archive \
-workspace YourApp.xcworkspace \
-scheme YourApp \
-archivePath YourApp.xcarchive \
-configuration Release \
CODE_SIGN_IDENTITY="${{ secrets.IOS_CODE_SIGN_IDENTITY }}" \
PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}"
- name: Export IPA
run: |
cd ios
xcodebuild -exportArchive \
-archivePath YourApp.xcarchive \
-exportPath . \
-exportOptionsPlist ExportOptions.plist
- name: Upload iOS IPA
uses: actions/upload-artifact@v3
with:
name: ios-ipa
path: ios/YourApp.ipa
- name: Upload to App Store Connect
if: startsWith(github.ref, 'refs/tags/v')
run: |
cd ios
bundle exec fastlane upload_to_testflight
deploy-staging:
needs: [build-android, build-ios]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
groups: testers
file: android/app/build/outputs/bundle/release/app-release.aab
releaseNotes: |
Automated staging build from commit ${{ github.sha }}
Changes in this build:
${{ github.event.head_commit.message }}
create-release:
needs: [build-android, build-ios]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
- name: Generate Release Notes
id: release-notes
run: |
VERSION=${GITHUB_REF#refs/tags/}
CHANGELOG=$(git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD)
cat > release-notes.md << EOF
## Release $VERSION
### Changes
$CHANGELOG
### Downloads
- Android: Available on Google Play Store
- iOS: Available on App Store
EOF
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.release-notes.outputs.version }}
body_path: release-notes.md
draft: false
prerelease: false
Fastlane Configuration
ruby
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
before_all do
ensure_git_status_clean
setup_circle_ci if ENV['CI']
end
desc "Run tests"
lane :test do
scan(
workspace: "YourApp.xcworkspace",
scheme: "YourApp",
clean: true,
output_directory: "./test_output",
output_types: "html,junit"
)
end
desc "Build for testing"
lane :build_for_testing do
gym(
workspace: "YourApp.xcworkspace",
scheme: "YourApp",
configuration: "Debug",
clean: true,
build_path: "./build",
output_directory: "./build",
skip_package_ipa: true,
skip_archive: true,
destination: "generic/platform=iOS Simulator"
)
end
desc "Build and upload to TestFlight"
lane :beta do
ensure_git_branch(branch: 'main')
increment_build_number(
build_number: latest_testflight_build_number + 1
)
gym(
workspace: "YourApp.xcworkspace",
scheme: "YourApp",
configuration: "Release",
clean: true,
export_method: "app-store",
output_directory: "./build"
)
upload_to_testflight(
skip_waiting_for_build_processing: false,
notify_external_testers: true,
groups: ["Beta Testers"],
changelog: changelog_from_git_commits(
commits_count: 10,
pretty: "- %s"
)
)
slack(
message: "New iOS beta build uploaded to TestFlight! 🚀",
success: true,
channel: "#mobile-releases"
)
end
desc "Release to App Store"
lane :release do
ensure_git_branch(branch: 'main')
ensure_git_status_clean
version = get_version_number(xcodeproj: "YourApp.xcodeproj")
gym(
workspace: "YourApp.xcworkspace",
scheme: "YourApp",
configuration: "Release",
clean: true,
export_method: "app-store"
)
deliver(
submit_for_review: false,
automatic_release: false,
force: true,
metadata_path: "./fastlane/metadata",
screenshots_path: "./fastlane/screenshots",
skip_binary_upload: false,
skip_screenshots: false,
skip_metadata: false
)
slack(
message: "iOS version #{version} submitted to App Store! 📱",
success: true,
channel: "#mobile-releases"
)
end
desc "Update certificates and provisioning profiles"
lane :certificates do
match(
type: "development",
app_identifier: "com.yourcompany.yourapp"
)
match(
type: "appstore",
app_identifier: "com.yourcompany.yourapp"
)
end
error do |lane, exception|
slack(
message: "iOS build failed in lane #{lane}: #{exception.message}",
success: false,
channel: "#mobile-releases"
)
end
end
platform :android do
desc "Run tests"
lane :test do
gradle(task: "test")
end
desc "Build debug APK"
lane :debug do
gradle(task: "assembleDebug")
end
desc "Build and upload to Play Console internal track"
lane :beta do
gradle(
task: "bundleRelease",
properties: {
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"]
}
)
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
release_status: "completed"
)
slack(
message: "New Android beta build uploaded to Play Console! 🤖",
success: true,
channel: "#mobile-releases"
)
end
desc "Release to Play Store"
lane :release do
gradle(
task: "bundleRelease",
properties: {
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"]
}
)
upload_to_play_store(
track: "production",
aab: "app/build/outputs/bundle/release/app-release.aab",
release_status: "draft"
)
slack(
message: "Android release uploaded to Play Store! 🎉",
success: true,
channel: "#mobile-releases"
)
end
error do |lane, exception|
slack(
message: "Android build failed in lane #{lane}: #{exception.message}",
success: false,
channel: "#mobile-releases"
)
end
end
Version Management System
Semantic Versioning Implementation
javascript
// scripts/version-manager.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
class VersionManager {
constructor() {
this.packageJsonPath = path.join(process.cwd(), 'package.json');
this.androidGradlePath = path.join(process.cwd(), 'android/app/build.gradle');
this.iosProjectPath = path.join(process.cwd(), 'ios/YourApp.xcodeproj/project.pbxproj');
this.iosInfoPlistPath = path.join(process.cwd(), 'ios/YourApp/Info.plist');
}
getCurrentVersion() {
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
return packageJson.version;
}
incrementVersion(type = 'patch') {
const currentVersion = this.getCurrentVersion();
const [major, minor, patch] = currentVersion.split('.').map(Number);
let newVersion;
switch (type) {
case 'major':
newVersion = `${major + 1}.0.0`;
break;
case 'minor':
newVersion = `${major}.${minor + 1}.0`;
break;
case 'patch':
default:
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}
this.updateAllVersions(newVersion);
return newVersion;
}
updateAllVersions(version) {
this.updatePackageJson(version);
this.updateAndroidVersion(version);
this.updateiOSVersion(version);
console.log(`✅ Updated all platform versions to ${version}`);
}
updatePackageJson(version) {
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
packageJson.version = version;
fs.writeFileSync(this.packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`Updated package.json to version ${version}`);
}
updateAndroidVersion(version) {
let gradleContent = fs.readFileSync(this.androidGradlePath, 'utf8');
// Update versionName
gradleContent = gradleContent.replace(
/versionName\s+"[^"]+"/,
`versionName "${version}"`
);
// Increment versionCode
const versionCodeMatch = gradleContent.match(/versionCode\s+(\d+)/);
if (versionCodeMatch) {
const currentVersionCode = parseInt(versionCodeMatch[1]);
const newVersionCode = currentVersionCode + 1;
gradleContent = gradleContent.replace(
/versionCode\s+\d+/,
`versionCode ${newVersionCode}`
);
}
fs.writeFileSync(this.androidGradlePath, gradleContent);
console.log(`Updated Android version to ${version}`);
}
updateiOSVersion(version) {
// Update Xcode project
if (fs.existsSync(this.iosProjectPath)) {
try {
execSync(`agvtool new-marketing-version ${version}`, {
cwd: path.dirname(this.iosProjectPath),
stdio: 'pipe'
});
execSync(`agvtool next-version -all`, {
cwd: path.dirname(this.iosProjectPath),
stdio: 'pipe'
});
console.log(`Updated iOS version to ${version}`);
} catch (error) {
console.warn('Could not update iOS version automatically. Please update manually.');
}
}
}
generateChangelog(version) {
try {
const gitLog = execSync('git log --oneline --since="1 week ago"', { encoding: 'utf8' });
const commits = gitLog.trim().split('\n').filter(line => line.length > 0);
const changelog = `
## Version ${version} - ${new Date().toISOString().split('T')[0]}
### Changes
${commits.map(commit => `- ${commit.substring(8)}`).join('\n')}
### Technical Details
- Build Date: ${new Date().toISOString()}
- Git Commit: ${execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim()}
- Environment: ${process.env.NODE_ENV || 'development'}
`;
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
let existingChangelog = '';
if (fs.existsSync(changelogPath)) {
existingChangelog = fs.readFileSync(changelogPath, 'utf8');
}
const newChangelog = changelog + '\n' + existingChangelog;
fs.writeFileSync(changelogPath, newChangelog);
console.log(`✅ Generated changelog for version ${version}`);
return changelog;
} catch (error) {
console.warn('Could not generate changelog:', error.message);
return null;
}
}
createGitTag(version) {
try {
execSync(`git add .`);
execSync(`git commit -m "chore: bump version to ${version}"`);
execSync(`git tag -a v${version} -m "Release version ${version}"`);
console.log(`✅ Created git tag v${version}`);
return true;
} catch (error) {
console.error('Failed to create git tag:', error.message);
return false;
}
}
validateVersion(version) {
const semverRegex = /^(\d+)\.(\d+)\.(\d+)$/;
return semverRegex.test(version);
}
}
// CLI Interface
if (require.main === module) {
const versionManager = new VersionManager();
const args = process.argv.slice(2);
const command = args[0];
const versionType = args[1] || 'patch';
switch (command) {
case 'current':
console.log(`Current version: ${versionManager.getCurrentVersion()}`);
break;
case 'increment':
const newVersion = versionManager.incrementVersion(versionType);
versionManager.generateChangelog(newVersion);
versionManager.createGitTag(newVersion);
break;
case 'set':
const targetVersion = args[1];
if (!targetVersion || !versionManager.validateVersion(targetVersion)) {
console.error('Please provide a valid semantic version (e.g., 1.2.3)');
process.exit(1);
}
versionManager.updateAllVersions(targetVersion);
versionManager.generateChangelog(targetVersion);
versionManager.createGitTag(targetVersion);
break;
default:
console.log(`
Usage: node version-manager.js <command> [options]
Commands:
current Show current version
increment [type] Increment version (patch|minor|major)
set <version> Set specific version
Examples:
node version-manager.js current
node version-manager.js increment patch
node version-manager.js increment minor
node version-manager.js set 2.0.0
`);
}
}
module.exports = VersionManager;
Release Orchestration
Release Coordinator Service
javascript
// release-coordinator.js
const { WebClient } = require('@slack/web-api');
const { Octokit } = require('@octokit/rest');
const { GoogleAuth } = require('google-auth-library');
class ReleaseCoordinator {
constructor(config) {
this.config = config;
this.slack = new WebClient(config.slackToken);
this.github = new Octokit({ auth: config.githubToken });
this.playStoreAuth = new GoogleAuth({
keyFile: config.googleServiceAccountPath,
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
}
async orchestrateRelease(version, releaseType = 'patch') {
const releaseId = `release-${version}-${Date.now()}`;
try {
await this.notifyReleaseStart(releaseId, version, releaseType);
// Step 1: Create release branch
await this.createReleaseBranch(version);
// Step 2: Run automated tests
const testResults = await this.runAutomatedTests();
if (!testResults.success) {
throw new Error(`Tests failed: ${testResults.errors.join(', ')}`);
}
// Step 3: Build and distribute to internal testers
await this.buildAndDistribute(version, 'internal');
// Step 4: Wait for QA approval
const qaApproval = await this.waitForQAApproval(releaseId);
if (!qaApproval) {
throw new Error('QA approval not received');
}
// Step 5: Deploy to stores
await this.deployToStores(version);
// Step 6: Monitor deployment
await this.monitorDeployment(version);
await this.notifyReleaseSuccess(releaseId, version);
} catch (error) {
await this.notifyReleaseError(releaseId, version, error);
throw error;
}
}
async createReleaseBranch(version) {
const { data: mainBranch } = await this.github.rest.repos.getBranch({
owner: this.config.repoOwner,
repo: this.config.repoName,
branch: 'main',
});
await this.github.rest.git.createRef({
owner: this.config.repoOwner,
repo: this.config.repoName,
ref: `refs/heads/release/${version}`,
sha: mainBranch.commit.sha,
});
console.log(`✅ Created release branch: release/${version}`);
}
async runAutomatedTests() {
// Trigger GitHub Actions workflow for testing
const { data: workflow } = await this.github.rest.actions.createWorkflowDispatch({
owner: this.config.repoOwner,
repo: this.config.repoName,
workflow_id: 'test.yml',
ref: 'main',
});
// Poll for completion
return await this.pollWorkflowCompletion(workflow.id);
}
async buildAndDistribute(version, track = 'internal') {
const builds = await Promise.all([
this.buildAndroid(version, track),
this.buildiOS(version, track),
]);
return builds;
}
async buildAndroid(version, track) {
// Trigger Android build workflow
const { data: workflow } = await this.github.rest.actions.createWorkflowDispatch({
owner: this.config.repoOwner,
repo: this.config.repoName,
workflow_id: 'android-build.yml',
ref: `release/${version}`,
inputs: {
version,
track,
},
});
return await this.pollWorkflowCompletion(workflow.id);
}
async buildiOS(version, track) {
// Trigger iOS build workflow
const { data: workflow } = await this.github.rest.actions.createWorkflowDispatch({
owner: this.config.repoOwner,
repo: this.config.repoName,
workflow_id: 'ios-build.yml',
ref: `release/${version}`,
inputs: {
version,
track,
},
});
return await this.pollWorkflowCompletion(workflow.id);
}
async waitForQAApproval(releaseId, timeout = 24 * 60 * 60 * 1000) {
return new Promise((resolve) => {
const startTime = Date.now();
const checkApproval = async () => {
if (Date.now() - startTime > timeout) {
resolve(false);
return;
}
// Check approval status (implement your approval mechanism)
const approved = await this.checkApprovalStatus(releaseId);
if (approved) {
resolve(true);
} else {
setTimeout(checkApproval, 5 * 60 * 1000); // Check every 5 minutes
}
};
checkApproval();
});
}
async deployToStores(version) {
const deployments = await Promise.all([
this.deployToPlayStore(version),
this.deployToAppStore(version),
]);
return deployments;
}
async deployToPlayStore(version) {
// Use Google Play Developer API to promote internal track to production
const auth = await this.playStoreAuth.getClient();
// Implementation would involve calling Play Developer API
console.log(`🤖 Deploying Android version ${version} to Play Store`);
return { platform: 'android', version, status: 'deployed' };
}
async deployToAppStore(version) {
// Use App Store Connect API to submit for review
console.log(`📱 Deploying iOS version ${version} to App Store`);
return { platform: 'ios', version, status: 'submitted' };
}
async monitorDeployment(version) {
// Monitor app store rollout status
console.log(`👀 Monitoring deployment of version ${version}`);
// Check for crashes, user feedback, performance metrics
await this.monitorCrashReports(version);
await this.monitorUserFeedback(version);
await this.monitorPerformanceMetrics(version);
}
async notifyReleaseStart(releaseId, version, releaseType) {
await this.slack.chat.postMessage({
channel: this.config.slackChannel,
text: `🚀 Starting ${releaseType} release ${version}`,
blocks: [
{
type: 'section',
text: {
type: 'markdown',
text: `*Release Started* 🚀\n\n*Version:* ${version}\n*Type:* ${releaseType}\n*Release ID:* ${releaseId}`
}
}
]
});
}
async notifyReleaseSuccess(releaseId, version) {
await this.slack.chat.postMessage({
channel: this.config.slackChannel,
text: `✅ Release ${version} completed successfully!`,
blocks: [
{
type: 'section',
text: {
type: 'markdown',
text: `*Release Completed* ✅\n\n*Version:* ${version}\n*Release ID:* ${releaseId}\n\nThe app is now available on both app stores! 🎉`
}
}
]
});
}
async notifyReleaseError(releaseId, version, error) {
await this.slack.chat.postMessage({
channel: this.config.slackChannel,
text: `❌ Release ${version} failed`,
blocks: [
{
type: 'section',
text: {
type: 'markdown',
text: `*Release Failed* ❌\n\n*Version:* ${version}\n*Release ID:* ${releaseId}\n*Error:* ${error.message}`
}
}
]
});
}
async pollWorkflowCompletion(workflowId, timeout = 30 * 60 * 1000) {
const startTime = Date.now();
return new Promise((resolve) => {
const checkStatus = async () => {
if (Date.now() - startTime > timeout) {
resolve({ success: false, error: 'Workflow timeout' });
return;
}
try {
const { data: workflow } = await this.github.rest.actions.getWorkflowRun({
owner: this.config.repoOwner,
repo: this.config.repoName,
run_id: workflowId,
});
if (workflow.status === 'completed') {
resolve({
success: workflow.conclusion === 'success',
conclusion: workflow.conclusion
});
} else {
setTimeout(checkStatus, 30000); // Check every 30 seconds
}
} catch (error) {
resolve({ success: false, error: error.message });
}
};
checkStatus();
});
}
}
module.exports = ReleaseCoordinator;
Store Management
App Store Connect Integration
javascript
// app-store-manager.js
const jwt = require('jsonwebtoken');
const axios = require('axios');
const fs = require('fs');
class AppStoreManager {
constructor(config) {
this.config = config;
this.baseURL = 'https://api.appstoreconnect.apple.com/v1';
}
generateJWT() {
const privateKey = fs.readFileSync(this.config.privateKeyPath, 'utf8');
const payload = {
iss: this.config.issuerId,
exp: Math.floor(Date.now() / 1000) + (20 * 60), // 20 minutes
aud: 'appstoreconnect-v1',
};
return jwt.sign(payload, privateKey, {
algorithm: 'ES256',
header: {
kid: this.config.keyId,
typ: 'JWT',
},
});
}
async makeRequest(endpoint, method = 'GET', data = null) {
const token = this.generateJWT();
const response = await axios({
method,
url: `${this.baseURL}${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
data,
});
return response.data;
}
async getApps() {
const response = await this.makeRequest('/apps');
return response.data;
}
async getAppInfo(appId) {
const response = await this.makeRequest(`/apps/${appId}`);
return response.data;
}
async getBuilds(appId) {
const response = await this.makeRequest(`/apps/${appId}/builds`);
return response.data;
}
async createVersion(appId, versionString, platform = 'IOS') {
const data = {
type: 'appStoreVersions',
attributes: {
platform,
versionString,
},
relationships: {
app: {
data: {
type: 'apps',
id: appId,
},
},
},
};
const response = await this.makeRequest('/appStoreVersions', 'POST', { data });
return response.data;
}
async submitForReview(versionId) {
const data = {
type: 'appStoreVersionSubmissions',
relationships: {
appStoreVersion: {
data: {
type: 'appStoreVersions',
id: versionId,
},
},
},
};
const response = await this.makeRequest('/appStoreVersionSubmissions', 'POST', { data });
return response.data;
}
async getReviewStatus(appId) {
const response = await this.makeRequest(`/apps/${appId}/appStoreVersions?filter[appStoreState]=PENDING_APPLE_RELEASE,IN_REVIEW,PENDING_DEVELOPER_RELEASE`);
return response.data;
}
async updateMetadata(versionId, metadata) {
const data = {
type: 'appStoreVersions',
id: versionId,
attributes: metadata,
};
const response = await this.makeRequest(`/appStoreVersions/${versionId}`, 'PATCH', { data });
return response.data;
}
async uploadScreenshots(versionId, screenshots) {
// Implementation for screenshot upload would go here
// This involves creating screenshot sets and uploading images
console.log(`Uploading screenshots for version ${versionId}`);
}
async monitorReviewStatus(appId, callback) {
const checkStatus = async () => {
try {
const status = await this.getReviewStatus(appId);
callback(null, status);
} catch (error) {
callback(error, null);
}
};
// Check every 10 minutes
const interval = setInterval(checkStatus, 10 * 60 * 1000);
checkStatus(); // Initial check
return interval;
}
}
module.exports = AppStoreManager;
Google Play Console Integration
javascript
// play-store-manager.js
const { google } = require('googleapis');
const fs = require('fs');
class PlayStoreManager {
constructor(config) {
this.config = config;
this.auth = new google.auth.GoogleAuth({
keyFile: config.serviceAccountPath,
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
this.androidpublisher = google.androidpublisher('v3');
}
async getAuthClient() {
if (!this.authClient) {
this.authClient = await this.auth.getClient();
}
return this.authClient;
}
async createEdit(packageName) {
const auth = await this.getAuthClient();
const response = await this.androidpublisher.edits.insert({
auth,
packageName,
});
return response.data.id;
}
async commitEdit(packageName, editId) {
const auth = await this.getAuthClient();
await this.androidpublisher.edits.commit({
auth,
packageName,
editId,
});
}
async uploadBundle(packageName, editId, bundlePath) {
const auth = await this.getAuthClient();
const response = await this.androidpublisher.edits.bundles.upload({
auth,
packageName,
editId,
media: {
mimeType: 'application/octet-stream',
body: fs.createReadStream(bundlePath),
},
});
return response.data.versionCode;
}
async updateTrack(packageName, editId, track, versionCodes, releaseStatus = 'completed') {
const auth = await this.getAuthClient();
await this.androidpublisher.edits.tracks.update({
auth,
packageName,
editId,
track,
requestBody: {
releases: [{
versionCodes: versionCodes.map(String),
status: releaseStatus,
}],
},
});
}
async getTrackInfo(packageName, track) {
const auth = await this.getAuthClient();
const response = await this.androidpublisher.edits.tracks.get({
auth,
packageName,
track,
});
return response.data;
}
async promoteRelease(packageName, fromTrack, toTrack, versionCodes) {
const editId = await this.createEdit(packageName);
try {
await this.updateTrack(packageName, editId, toTrack, versionCodes);
await this.commitEdit(packageName, editId);
console.log(`✅ Promoted release from ${fromTrack} to ${toTrack}`);
} catch (error) {
console.error(`❌ Failed to promote release: ${error.message}`);
throw error;
}
}
async getRolloutInfo(packageName, track) {
const auth = await this.getAuthClient();
const response = await this.androidpublisher.edits.tracks.get({
auth,
packageName,
track,
});
const release = response.data.releases?.[0];
return {
status: release?.status,
userFraction: release?.userFraction,
versionCodes: release?.versionCodes,
};
}
async updateRolloutPercentage(packageName, track, versionCodes, userFraction) {
const editId = await this.createEdit(packageName);
try {
const auth = await this.getAuthClient();
await this.androidpublisher.edits.tracks.update({
auth,
packageName,
editId,
track,
requestBody: {
releases: [{
versionCodes: versionCodes.map(String),
status: 'inProgress',
userFraction,
}],
},
});
await this.commitEdit(packageName, editId);
console.log(`✅ Updated rollout to ${userFraction * 100}%`);
} catch (error) {
console.error(`❌ Failed to update rollout: ${error.message}`);
throw error;
}
}
async haltRollout(packageName, track, versionCodes) {
const editId = await this.createEdit(packageName);
try {
const auth = await this.getAuthClient();
await this.androidpublisher.edits.tracks.update({
auth,
packageName,
editId,
track,
requestBody: {
releases: [{
versionCodes: versionCodes.map(String),
status: 'halted',
}],
},
});
await this.commitEdit(packageName, editId);
console.log(`🛑 Halted rollout for track ${track}`);
} catch (error) {
console.error(`❌ Failed to halt rollout: ${error.message}`);
throw error;
}
}
}
module.exports = PlayStoreManager;
Release Monitoring
Release Health Monitor
javascript
// release-monitor.js
const axios = require('axios');
class ReleaseHealthMonitor {
constructor(config) {
this.config = config;
this.metrics = {
crashRate: 0,
anrRate: 0,
adoptionRate: 0,
userRating: 0,
performanceMetrics: {},
};
}
async startMonitoring(version, duration = 24 * 60 * 60 * 1000) {
console.log(`🔍 Starting health monitoring for version ${version}`);
const monitoringTasks = [
this.monitorCrashRates(version),
this.monitorAdoptionRate(version),
this.monitorUserFeedback(version),
this.monitorPerformanceMetrics(version),
];
// Set up periodic checks
const interval = setInterval(async () => {
await Promise.all(monitoringTasks.map(task => task.catch(console.error)));
await this.evaluateReleaseHealth(version);
}, 5 * 60 * 1000); // Check every 5 minutes
// Stop monitoring after specified duration
setTimeout(() => {
clearInterval(interval);
console.log(`✅ Completed health monitoring for version ${version}`);
}, duration);
return interval;
}
async monitorCrashRates(version) {
try {
// Firebase Crashlytics API call
const response = await axios.get(
`https://crashlytics.googleapis.com/v1beta/apps/${this.config.firebaseAppId}/crashlytics/versions/${version}`,
{
headers: {
'Authorization': `Bearer ${this.config.firebaseToken}`,
},
}
);
const crashData = response.data;
this.metrics.crashRate = crashData.crashRate || 0;
this.metrics.anrRate = crashData.anrRate || 0;
if (this.metrics.crashRate > this.config.thresholds.maxCrashRate) {
await this.triggerAlert('HIGH_CRASH_RATE', {
version,
crashRate: this.metrics.crashRate,
});
}
} catch (error) {
console.error('Error monitoring crash rates:', error.message);
}
}
async monitorAdoptionRate(version) {
try {
// Google Analytics or custom analytics API
const response = await axios.get(
`${this.config.analyticsEndpoint}/adoption-rate`,
{
headers: {
'Authorization': `Bearer ${this.config.analyticsToken}`,
},
params: {
version,
timeframe: '24h',
},
}
);
this.metrics.adoptionRate = response.data.adoptionRate;
if (this.metrics.adoptionRate < this.config.thresholds.minAdoptionRate) {
await this.triggerAlert('LOW_ADOPTION_RATE', {
version,
adoptionRate: this.metrics.adoptionRate,
});
}
} catch (error) {
console.error('Error monitoring adoption rate:', error.message);
}
}
async monitorUserFeedback(version) {
try {
// App Store and Play Store API calls to get reviews
const [iosReviews, androidReviews] = await Promise.all([
this.getiOSReviews(version),
this.getAndroidReviews(version),
]);
const allReviews = [...iosReviews, ...androidReviews];
const averageRating = allReviews.reduce((sum, review) => sum + review.rating, 0) / allReviews.length;
this.metrics.userRating = averageRating;
if (averageRating < this.config.thresholds.minUserRating) {
await this.triggerAlert('LOW_USER_RATING', {
version,
rating: averageRating,
reviewCount: allReviews.length,
});
}
} catch (error) {
console.error('Error monitoring user feedback:', error.message);
}
}
async monitorPerformanceMetrics(version) {
try {
// Firebase Performance Monitoring API
const response = await axios.get(
`${this.config.performanceEndpoint}/metrics`,
{
headers: {
'Authorization': `Bearer ${this.config.firebaseToken}`,
},
params: {
version,
metrics: 'app_start_time,screen_rendering,network_requests',
},
}
);
this.metrics.performanceMetrics = response.data;
// Check performance regressions
const appStartTime = this.metrics.performanceMetrics.app_start_time;
if (appStartTime > this.config.thresholds.maxAppStartTime) {
await this.triggerAlert('PERFORMANCE_REGRESSION', {
version,
metric: 'app_start_time',
value: appStartTime,
});
}
} catch (error) {
console.error('Error monitoring performance metrics:', error.message);
}
}
async evaluateReleaseHealth(version) {
const healthScore = this.calculateHealthScore();
console.log(`📊 Release Health Score for ${version}: ${healthScore}/100`);
if (healthScore < this.config.thresholds.minHealthScore) {
await this.triggerAlert('POOR_RELEASE_HEALTH', {
version,
healthScore,
metrics: this.metrics,
});
// Consider automatic rollback
if (healthScore < this.config.thresholds.rollbackThreshold) {
await this.initiateRollback(version);
}
}
}
calculateHealthScore() {
const weights = {
crashRate: 30,
adoptionRate: 25,
userRating: 25,
performance: 20,
};
const scores = {
crashRate: Math.max(0, 100 - (this.metrics.crashRate * 1000)), // Assuming crash rate is a decimal
adoptionRate: Math.min(100, this.metrics.adoptionRate * 100),
userRating: (this.metrics.userRating / 5) * 100,
performance: this.calculatePerformanceScore(),
};
return Object.keys(weights).reduce((total, metric) => {
return total + (scores[metric] * weights[metric] / 100);
}, 0);
}
calculatePerformanceScore() {
// Simplified performance scoring based on app start time
const appStartTime = this.metrics.performanceMetrics.app_start_time || 1000;
const maxAcceptableTime = 3000; // 3 seconds
return Math.max(0, 100 - ((appStartTime / maxAcceptableTime) * 100));
}
async triggerAlert(alertType, data) {
const alert = {
type: alertType,
timestamp: new Date().toISOString(),
data,
severity: this.getAlertSeverity(alertType),
};
console.log(`🚨 ALERT: ${alertType}`, alert);
// Send to Slack, PagerDuty, etc.
await this.sendSlackAlert(alert);
if (alert.severity === 'critical') {
await this.sendPagerDutyAlert(alert);
}
}
getAlertSeverity(alertType) {
const severityMap = {
HIGH_CRASH_RATE: 'critical',
LOW_ADOPTION_RATE: 'warning',
LOW_USER_RATING: 'warning',
PERFORMANCE_REGRESSION: 'warning',
POOR_RELEASE_HEALTH: 'critical',
};
return severityMap[alertType] || 'info';
}
async initiateRollback(version) {
console.log(`🔄 Initiating automatic rollback for version ${version}`);
// Implementation would involve calling app store APIs to rollback
// This is a critical operation and should have proper safeguards
await this.triggerAlert('AUTOMATIC_ROLLBACK_INITIATED', {
version,
reason: 'Poor release health score',
healthScore: this.calculateHealthScore(),
});
}
}
module.exports = ReleaseHealthMonitor;
Best Practices
1. Release Planning
- Define clear release criteria
- Plan rollback strategies
- Set up proper environments
- Document release processes
2. Automation
- Automate build and deployment
- Implement automated testing
- Use infrastructure as code
- Set up monitoring and alerting
3. Quality Assurance
- Implement staged rollouts
- Monitor key metrics
- Have rollback procedures
- Test on multiple devices/OS versions
4. Communication
- Keep stakeholders informed
- Document release notes
- Coordinate across teams
- Maintain release calendar
5. Post-Release
- Monitor app performance
- Track user feedback
- Analyze crash reports
- Plan hotfixes if needed
This comprehensive release management system provides enterprise-grade deployment automation, monitoring, and coordination capabilities for mobile applications across both major app stores.