Skip to content

Mobile Release Integration

This guide covers how to integrate mobile app releases with your CI/CD pipeline, including automated version management, release coordination, and deployment strategies.

The PersiaNation mobile app uses an integrated release process that coordinates:

  • Version Management: Automated version bumping and tagging
  • Build Automation: Triggered builds on release events
  • Store Submission: Automated app store deployments
  • OTA Coordination: Managing over-the-air updates alongside releases
  • Release Notes: Automated changelog generation

Create .github/workflows/version-bump.yml:

name: Version Bump
on:
workflow_dispatch:
inputs:
version_type:
description: "Version bump type"
required: true
default: "patch"
type: choice
options:
- major
- minor
- patch
- prerelease
jobs:
version-bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18.x
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Install dependencies
run: pnpm install
- name: Configure git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump version
run: |
# Update package.json version
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
# Get new version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
# Update app.config.ts version
sed -i "s/VERSION: '.*'/VERSION: '$NEW_VERSION'/" env.js
- name: Update build number
run: |
# Increment build number
CURRENT_BUILD=$(grep -o "BUILD_NUMBER: '[^']*'" env.js | sed "s/BUILD_NUMBER: '//;s/'//")
NEW_BUILD=$((CURRENT_BUILD + 1))
sed -i "s/BUILD_NUMBER: '.*'/BUILD_NUMBER: '$NEW_BUILD'/" env.js
echo "NEW_BUILD=$NEW_BUILD" >> $GITHUB_ENV
- name: Commit changes
run: |
git add package.json env.js
git commit -m "chore: bump version to $NEW_VERSION (build $NEW_BUILD)"
git push origin main
- name: Create release tag
run: |
git tag "v$NEW_VERSION"
git push origin "v$NEW_VERSION"
- name: Create release PR
run: |
gh pr create \
--title "Release v$NEW_VERSION" \
--body "Automated release preparation for version $NEW_VERSION" \
--base main \
--head main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
scripts/get-version.ts
import { execSync } from "child_process";
import { readFileSync } from "fs";
export function getCurrentVersion(): string {
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
return packageJson.version;
}
export function getNextVersion(type: "major" | "minor" | "patch"): string {
const current = getCurrentVersion();
const [major, minor, patch] = current.split(".").map(Number);
switch (type) {
case "major":
return `${major + 1}.0.0`;
case "minor":
return `${major}.${minor + 1}.0`;
case "patch":
return `${major}.${minor}.${patch + 1}`;
default:
throw new Error(`Invalid version type: ${type}`);
}
}
export function getBuildNumber(): number {
try {
const gitCount = execSync("git rev-list --count HEAD", {
encoding: "utf8",
});
return parseInt(gitCount.trim());
} catch {
return Date.now();
}
}

Create .github/workflows/release-build.yml:

name: Release Build
on:
release:
types: [published]
jobs:
build-and-submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.x
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: Build for production
run: |
eas build \
--platform all \
--profile production \
--non-interactive \
--wait
- name: Submit to stores
if: github.event.release.prerelease == false
run: |
eas submit \
--platform all \
--profile production \
--latest \
--non-interactive
- name: Deploy OTA update
run: |
eas update \
--branch production-branch \
--message "Release ${{ github.event.release.tag_name }}" \
--auto
- name: Update release with build info
run: |
# Get build URLs
BUILD_INFO=$(eas build:list --limit=1 --json)
# Update GitHub release with build information
gh release edit ${{ github.event.release.tag_name }} \
--notes "${{ github.event.release.body }}
## Build Information
- iOS Build: [View in EAS](https://expo.dev/builds/...)
- Android Build: [View in EAS](https://expo.dev/builds/...)
- OTA Update: Deployed to production channel"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Release Pipeline
on:
push:
tags:
- "v*"
jobs:
validate:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is-prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
- name: Extract version info
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
if [[ $VERSION == *"-"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
staging-build:
needs: validate
if: needs.validate.outputs.is-prerelease == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: expo/expo-github-action@v8
with:
token: ${{ secrets.EXPO_TOKEN }}
- name: Build staging
run: |
eas build \
--platform all \
--profile staging \
--non-interactive
production-build:
needs: validate
if: needs.validate.outputs.is-prerelease == 'false'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: expo/expo-github-action@v8
with:
token: ${{ secrets.EXPO_TOKEN }}
- name: Build production
run: |
eas build \
--platform all \
--profile production \
--non-interactive \
--wait
- name: Submit to stores
run: |
eas submit \
--platform all \
--profile production \
--latest \
--non-interactive
name: Generate Release Notes
on:
release:
types: [created]
jobs:
generate-notes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
run: |
# Get commits since last release
LAST_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
COMMITS=$(git log $LAST_TAG..HEAD --pretty=format:"- %s (%h)" --grep="feat:" --grep="fix:" --grep="BREAKING CHANGE:")
else
COMMITS=$(git log --pretty=format:"- %s (%h)" --grep="feat:" --grep="fix:" --grep="BREAKING CHANGE:")
fi
# Categorize commits
FEATURES=$(echo "$COMMITS" | grep "feat:" || echo "")
FIXES=$(echo "$COMMITS" | grep "fix:" || echo "")
BREAKING=$(echo "$COMMITS" | grep "BREAKING CHANGE:" || echo "")
# Generate release notes
NOTES="## What's New
$(if [ -n "$FEATURES" ]; then echo "### ✨ New Features"; echo "$FEATURES"; echo ""; fi)
$(if [ -n "$FIXES" ]; then echo "### 🐛 Bug Fixes"; echo "$FIXES"; echo ""; fi)
$(if [ -n "$BREAKING" ]; then echo "### ⚠️ Breaking Changes"; echo "$BREAKING"; echo ""; fi)
## Installation
### iOS
- Download from the App Store: [PersiaNation](https://apps.apple.com/app/...)
- TestFlight (Beta): [Join Beta](https://testflight.apple.com/join/...)
### Android
- Download from Google Play: [PersiaNation](https://play.google.com/store/apps/details?id=...)
- Internal Testing: Contact team for access
## Technical Details
- **Version**: ${{ github.event.release.tag_name }}
- **Build Date**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
- **Commit**: ${{ github.sha }}
- **EAS Build**: [View builds](https://expo.dev/builds)
"
# Save to file for multiline output
echo "$NOTES" > release_notes.md
- name: Update release
run: |
gh release edit ${{ github.event.release.tag_name }} \
--notes-file release_notes.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
scripts/deploy-ota.ts
import { execSync } from "child_process";
interface DeploymentConfig {
branch: string;
channel: string;
message: string;
rollout?: number;
}
export async function deployOTA(config: DeploymentConfig) {
const { branch, channel, message, rollout = 100 } = config;
console.log(`Deploying OTA update to ${channel} channel...`);
try {
// Deploy update
const command = `eas update --branch ${branch} --message "${message}" --auto`;
execSync(command, { stdio: "inherit" });
// If gradual rollout is specified
if (rollout < 100) {
console.log(`Setting gradual rollout to ${rollout}%`);
execSync(`eas update:configure --branch ${branch} --rollout ${rollout}`, {
stdio: "inherit",
});
}
console.log("OTA update deployed successfully!");
// Get update info
const updateInfo = execSync(
`eas update:list --branch ${branch} --limit 1 --json`,
{
encoding: "utf8",
}
);
return JSON.parse(updateInfo)[0];
} catch (error) {
console.error("OTA deployment failed:", error);
throw error;
}
}
// Usage in release workflow
export async function coordinateRelease(version: string) {
// Deploy to staging first
await deployOTA({
branch: "staging-branch",
channel: "staging",
message: `Release ${version} - Staging deployment`,
});
// Wait for QA approval (this would be manual step)
console.log("Waiting for QA approval...");
// Deploy to production with gradual rollout
await deployOTA({
branch: "production-branch",
channel: "production",
message: `Release ${version}`,
rollout: 25, // Start with 25% rollout
});
// Schedule full rollout after monitoring period
setTimeout(async () => {
await deployOTA({
branch: "production-branch",
channel: "production",
message: `Release ${version} - Full rollout`,
rollout: 100,
});
}, 24 * 60 * 60 * 1000); // 24 hours
}
name: Release Health Check
on:
release:
types: [published]
schedule:
- cron: "*/15 * * * *" # Every 15 minutes
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check app health metrics
run: |
# Check crash rates from app store APIs
# Check user feedback and ratings
# Monitor error reporting services
CRASH_RATE=$(curl -s "https://api.appstoreconnect.apple.com/v1/apps/.../crashRates" \
-H "Authorization: Bearer ${{ secrets.APP_STORE_CONNECT_TOKEN }}" \
| jq '.data[0].attributes.crashRate')
if (( $(echo "$CRASH_RATE > 0.05" | bc -l) )); then
echo "High crash rate detected: $CRASH_RATE"
exit 1
fi
- name: Rollback if unhealthy
if: failure()
run: |
# Rollback OTA update
eas update:rollback --branch production-branch --group-id previous
# Notify team
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-type: application/json' \
--data '{"text":"🚨 Release rollback triggered due to health check failure"}'
scripts/release-dashboard.ts
interface ReleaseMetrics {
version: string;
buildStatus: "pending" | "building" | "completed" | "failed";
otaStatus: "pending" | "deployed" | "rolled-back";
storeStatus: "pending" | "review" | "approved" | "rejected";
crashRate: number;
adoptionRate: number;
}
export class ReleaseDashboard {
async getMetrics(version: string): Promise<ReleaseMetrics> {
const [buildInfo, otaInfo, storeInfo, crashInfo] = await Promise.all([
this.getBuildStatus(version),
this.getOTAStatus(version),
this.getStoreStatus(version),
this.getCrashMetrics(version),
]);
return {
version,
buildStatus: buildInfo.status,
otaStatus: otaInfo.status,
storeStatus: storeInfo.status,
crashRate: crashInfo.rate,
adoptionRate: otaInfo.adoptionRate,
};
}
async generateReport(version: string): Promise<string> {
const metrics = await this.getMetrics(version);
return `
# Release Report: ${version}
## Build Status: ${metrics.buildStatus}
- iOS: ${metrics.buildStatus === "completed" ? "" : ""}
- Android: ${metrics.buildStatus === "completed" ? "" : ""}
## OTA Deployment: ${metrics.otaStatus}
- Adoption Rate: ${(metrics.adoptionRate * 100).toFixed(1)}%
- Status: ${metrics.otaStatus === "deployed" ? "" : ""}
## Store Status: ${metrics.storeStatus}
- iOS App Store: ${metrics.storeStatus}
- Google Play: ${metrics.storeStatus}
## Health Metrics
- Crash Rate: ${(metrics.crashRate * 100).toFixed(2)}%
- Status: ${metrics.crashRate < 0.05 ? "✅ Healthy" : "⚠️ Monitoring"}
---
*Generated at ${new Date().toISOString()}*
`.trim();
}
}
  • Schedule regular releases: Weekly or bi-weekly cadence
  • Feature freeze periods: Stop new features before releases
  • QA cycles: Dedicated testing time for each release
  • Rollback procedures: Always have a rollback plan ready
  • Fail fast: Stop the pipeline on critical failures
  • Gradual rollouts: Start with small percentages
  • Monitoring: Watch metrics closely after releases
  • Communication: Keep stakeholders informed
  • Semantic versioning: MAJOR.MINOR.PATCH format
  • Build numbers: Increment for each build
  • Branch strategy: Use release branches for stability
  • Tag consistency: Always tag releases in git
  • All tests pass: No releases with failing tests
  • Code review: Require reviews for release branches
  • Performance benchmarks: Meet performance criteria
  • Security scans: Pass security vulnerability checks
  1. Build failures during release

    • Check EAS build logs
    • Verify environment variables
    • Ensure dependencies are locked
  2. OTA update conflicts

    • Check channel configurations
    • Verify branch linkage
    • Review update compatibility
  3. Store submission rejections

    • Review app store guidelines
    • Check metadata and screenshots
    • Verify app signing certificates
Terminal window
# Check release status
gh release list
gh release view v1.2.3
# Monitor EAS builds
eas build:list --limit 10
eas build:view <build-id>
# Check OTA updates
eas update:list --branch production-branch
eas channel:list
# Verify store submissions
eas submit:list