SELF-832: tweak mobile deploy auto bump version pr feature; v2.7.0 (#1244)

* bump version to match staging

* save wip

* deploy fixes

* fix version setting

* update version logic

* fix version pr

* increase timeout to 2 hours

* pr logic tweaks

* fix script

* fix script path

* add comments and update logic to test from feature branch

* fix build path

* fix version input error

* fix pulling version

* add skip-deploy lable

* address cr concners
This commit is contained in:
Justin Hernandez
2025-10-09 23:58:29 -07:00
parent 3b4c937b73
commit a66c5907eb
7 changed files with 864 additions and 202 deletions

View File

@@ -25,28 +25,24 @@ runs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if there are changes
git add ${{ inputs.file_paths }}
if [[ -z $(git status -s) ]]; then
echo "No changes to commit. Skipping PR creation."
# Ensure we're on staging branch, not detached HEAD
git fetch origin staging dev
git checkout staging
# Check if staging has commits not in dev (version bumps + any build changes)
COMMITS_AHEAD=$(git rev-list --count origin/dev..staging)
if [ "$COMMITS_AHEAD" -eq 0 ]; then
echo " No new commits on staging compared to dev. Skipping PR creation."
exit 0
fi
# Commit changes on temporary branch
git checkout -b temp-version-commit
git commit -m "chore: bump ${{ inputs.platform }} version for ${{ inputs.version }} [skip ci]"
echo "📊 Staging is $COMMITS_AHEAD commit(s) ahead of dev"
# Create new branch from dev
git fetch origin dev
git checkout -b ${BRANCH_NAME} origin/dev
# Create new branch from current staging (which has all version changes)
git checkout -b ${BRANCH_NAME}
# Cherry-pick only the version changes
git cherry-pick temp-version-commit
# Clean up temporary branch
git branch -D temp-version-commit
# Push and create PR
# Push the branch
git push --set-upstream origin ${BRANCH_NAME}
# Determine PR title based on platform
@@ -60,6 +56,7 @@ runs:
--base dev \
--head ${BRANCH_NAME} \
--title "$PR_TITLE" \
--body "Automated version bump by CI"
--body "Automated version bump by CI" \
--label "automated"
env:
GH_TOKEN: ${{ inputs.github_token }}

View File

@@ -7,11 +7,18 @@ inputs:
description: "Path to the app directory"
required: true
outputs:
version:
description: "Extracted app version from package.json"
value: ${{ steps.get-version.outputs.version }}
runs:
using: "composite"
steps:
- name: Get version from package.json
id: get-version
shell: bash
run: |
VERSION=$(node -p "require('${{ inputs.app_path }}/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_ENV

View File

@@ -3,6 +3,30 @@ name: Mobile Deploy
# 1. Manual trigger (workflow_dispatch) with configurable options
# 2. When PRs are merged to staging (auto-deploy to internal track)
# 3. DISABLED: When called by other workflows (workflow_call)
#
# === PR LABELS ===
# - skip-deploy: Skip deployment entirely (no version bump, no builds)
# - version:major/minor/patch: Control version bump type
# - ios-only/android-only: Build only one platform
#
# === WORKFLOW LOGIC ===
# Build Branch: Always builds from the branch that triggered the workflow
# - PR merges: Builds from 'staging' branch (the branch being merged into)
# - Manual dispatch: Builds from the branch where workflow was manually triggered
# - This allows testing from feature branches before merging to dev/staging
#
# Version Bump PR: After successful build, creates PR to bump version
# - Default target: 'dev' branch (can be overridden with bump_target_branch input)
# - Workflow checks out the target branch, applies version changes, and creates PR
# - This separates the build source from the version bump destination
#
# Example flows:
# 1. Normal production flow:
# - Merge PR to staging → builds from staging → creates version bump PR to dev
# 2. Testing from feature branch:
# - Manually trigger from feature branch → builds from feature branch → creates version bump PR to dev
# 3. Custom version bump target:
# - Set bump_target_branch input → creates version bump PR to specified branch instead of dev
env:
# Build environment versions
@@ -71,6 +95,16 @@ on:
- patch
- minor
- major
dry_run:
description: "Do not commit/push or create PR/tags"
required: false
type: boolean
default: false
bump_target_branch:
description: "Target branch for version bump PR (default: dev). NOTE: This is where the version bump PR will be created, NOT the branch to build from. The workflow always builds from the triggering branch."
required: false
type: string
default: "dev"
pull_request:
types: [closed]
@@ -105,11 +139,110 @@ on:
default: false
concurrency:
# Group by deployment track or ref name to allow different tracks to run in parallel
# cancel-in-progress: false ensures we don't cancel ongoing deployments
# Branch-locking in create-version-bump-pr prevents duplicate PRs for same version
group: mobile-deploy-${{ inputs.deployment_track || github.ref_name }}
cancel-in-progress: false
jobs:
# Bump version atomically before platform builds to avoid race conditions
# NOTE: Checks out the triggering branch (staging for PR merges, or the branch where manually triggered)
bump-version:
runs-on: ubuntu-latest
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
!contains(github.event.pull_request.labels.*.name, 'skip-deploy')
outputs:
version: ${{ steps.bump.outputs.version }}
ios_build: ${{ steps.bump.outputs.ios_build }}
android_build: ${{ steps.bump.outputs.android_build }}
version_bump_type: ${{ steps.determine-bump.outputs.version_bump }}
platform: ${{ steps.determine-platform.outputs.platform }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Build from the branch that triggered the workflow (staging, feature branch, etc.)
ref: ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Determine version bump from PR labels or input
id: determine-bump
run: |
VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
# Override with PR label if present
if [ "${{ github.event_name }}" = "pull_request" ]; then
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q "version:major"; then
VERSION_BUMP="major"
elif echo "$LABELS" | grep -q "version:minor"; then
VERSION_BUMP="minor"
elif echo "$LABELS" | grep -q "version:patch"; then
VERSION_BUMP="patch"
fi
fi
echo "version_bump=$VERSION_BUMP" >> $GITHUB_OUTPUT
echo "📦 Version bump type: $VERSION_BUMP"
- name: Determine platform from labels or input
id: determine-platform
run: |
PLATFORM="both"
# Check workflow input first
if [ -n "${{ inputs.platform }}" ]; then
INPUT_PLATFORM="${{ inputs.platform }}"
if [ "$INPUT_PLATFORM" = "ios" ]; then
PLATFORM="ios"
elif [ "$INPUT_PLATFORM" = "android" ]; then
PLATFORM="android"
fi
fi
# Override with PR labels if present
if [ "${{ github.event_name }}" = "pull_request" ]; then
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q "ios-only"; then
PLATFORM="ios"
elif echo "$LABELS" | grep -q "android-only"; then
PLATFORM="android"
fi
fi
echo "platform=$PLATFORM" >> $GITHUB_OUTPUT
echo "📱 Platform to deploy: $PLATFORM"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Bump version using version-manager script
id: bump
run: |
cd ${{ env.APP_PATH }}
VERSION_BUMP="${{ steps.determine-bump.outputs.version_bump }}"
PLATFORM="${{ steps.determine-platform.outputs.platform }}"
echo "🔄 Calculating version bump..."
echo " Type: $VERSION_BUMP"
echo " Platform: $PLATFORM"
echo ""
# Use version-manager script to calculate bump
# NOTE: Using absolute path to ensure script is found regardless of CWD
node ${{ env.APP_PATH }}/scripts/version-manager.cjs bump "$VERSION_BUMP" "$PLATFORM"
echo ""
echo "✅ Version bump calculated successfully"
echo "⚠️ Note: Changes are local only. Will be committed in PR after successful builds."
build-ios:
needs: [bump-version]
runs-on: macos-latest-large
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
@@ -135,9 +268,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# When triggered by PR merge, use the merge commit on staging
# This ensures we deploy exactly what landed on staging (including version.json from source + any conflict resolutions)
ref: ${{ github.event.pull_request.merge_commit_sha || 'staging' }}
# Checkout the branch that triggered the workflow
ref: ${{ github.ref_name }}
- name: Read and sanitize Node.js version
shell: bash
run: |
@@ -152,26 +284,6 @@ jobs:
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Determine version bump from PR labels or input
id: version-bump
run: |
VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
# Override with PR label if present
if [ "${{ github.event_name }}" = "pull_request" ]; then
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q "version:major"; then
VERSION_BUMP="major"
elif echo "$LABELS" | grep -q "version:minor"; then
VERSION_BUMP="minor"
elif echo "$LABELS" | grep -q "version:patch"; then
VERSION_BUMP="patch"
fi
fi
echo "version_bump=$VERSION_BUMP" >> $GITHUB_OUTPUT
echo "📦 Version bump type: $VERSION_BUMP"
- name: Verify branch and commit (iOS)
if: inputs.platform != 'android'
run: |
@@ -179,21 +291,37 @@ jobs:
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
echo "Current commit: $(git rev-parse HEAD)"
echo "Current commit message: $(git log -1 --pretty=format:'%s')"
echo "Staging HEAD commit: $(git rev-parse origin/staging)"
echo "Staging HEAD message: $(git log -1 --pretty=format:'%s' origin/staging)"
BUILD_BRANCH="${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}"
echo "Building from branch: $BUILD_BRANCH"
echo "Target HEAD commit: $(git rev-parse origin/$BUILD_BRANCH)"
echo "Target HEAD message: $(git log -1 --pretty=format:'%s' origin/$BUILD_BRANCH)"
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "📌 Building from merge commit on staging (includes source + conflict resolutions)"
echo "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}"
echo "Merge commit includes version.json from source branch with bumped build numbers"
elif [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/staging)" ]; then
echo "⚠️ WARNING: Current commit differs from latest staging commit"
echo "This might indicate we're not building from the latest staging branch"
git log --oneline HEAD..origin/staging || true
elif [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/$BUILD_BRANCH)" ]; then
echo "⚠️ WARNING: Current commit differs from latest $BUILD_BRANCH commit"
echo "This might indicate we're not building from the latest $BUILD_BRANCH branch"
git log --oneline HEAD..origin/$BUILD_BRANCH || true
else
echo "✅ Building from latest staging commit"
echo "✅ Building from latest $BUILD_BRANCH commit"
fi
- name: Apply version bump for build
if: inputs.platform != 'android'
run: |
cd ${{ env.APP_PATH }}
VERSION="${{ needs.bump-version.outputs.version }}"
IOS_BUILD="${{ needs.bump-version.outputs.ios_build }}"
ANDROID_BUILD="${{ needs.bump-version.outputs.android_build }}"
echo "📝 Applying version bump for iOS build: $VERSION (iOS Build: $IOS_BUILD, Android Build: $ANDROID_BUILD)"
# Use version-manager script to apply versions
node ${{ env.APP_PATH }}/scripts/version-manager.cjs apply "$VERSION" "$IOS_BUILD" "$ANDROID_BUILD"
- name: Set up Xcode
if: inputs.platform != 'android'
uses: maxim-lobanov/setup-xcode@v1
@@ -510,6 +638,9 @@ jobs:
- name: Build and upload to App Store Connect/TestFlight
if: inputs.platform != 'android' && !env.ACT
env:
CI_VERSION: ${{ needs.bump-version.outputs.version }}
CI_IOS_BUILD: ${{ needs.bump-version.outputs.ios_build }}
CI_ANDROID_BUILD: ${{ needs.bump-version.outputs.android_build }}
ENABLE_DEBUG_LOGS: ${{ secrets.ENABLE_DEBUG_LOGS }}
GRAFANA_LOKI_PASSWORD: ${{ secrets.GRAFANA_LOKI_PASSWORD }}
GRAFANA_LOKI_URL: ${{ secrets.GRAFANA_LOKI_URL }}
@@ -546,29 +677,62 @@ jobs:
# Determine deployment track and version bump
DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}"
VERSION_BUMP="${{ steps.version-bump.outputs.version_bump }}"
VERSION_BUMP="${{ needs.bump-version.outputs.version_bump_type }}"
TEST_MODE="${{ inputs.test_mode || false }}"
echo "📱 Deployment Configuration:"
echo " - Track: $DEPLOYMENT_TRACK"
echo " - Version Bump: $VERSION_BUMP"
echo " - Version Bump: $VERSION_BUMP (already applied in bump-version job)"
echo " - Version: ${{ needs.bump-version.outputs.version }}"
echo " - iOS Build: ${{ needs.bump-version.outputs.ios_build }}"
echo " - Test Mode: $TEST_MODE"
if [ "$TEST_MODE" = "true" ]; then
echo "🧪 Running in TEST MODE - will skip upload to TestFlight"
bundle exec fastlane ios deploy_auto \
deployment_track:$DEPLOYMENT_TRACK \
version_bump:$VERSION_BUMP \
version_bump:skip \
test_mode:true \
--verbose
else
echo "🚀 Deploying to App Store Connect..."
bundle exec fastlane ios deploy_auto \
deployment_track:$DEPLOYMENT_TRACK \
version_bump:$VERSION_BUMP \
version_bump:skip \
--verbose
fi
- name: Verify iOS build output
if: inputs.platform != 'android'
run: |
cd ${{ env.APP_PATH }}
echo "🔍 Verifying iOS build artifacts..."
# Find the IPA file
IPA_PATH=$(find ios/build -name "*.ipa" 2>/dev/null | head -1)
if [ -z "$IPA_PATH" ]; then
echo "❌ ERROR: No IPA file found in ios/build directory"
echo "Build may have failed silently. Check Fastlane logs above."
exit 1
fi
echo "✅ Found IPA: $IPA_PATH"
# Check file size (should be at least 10MB for a real app)
IPA_SIZE=$(stat -f%z "$IPA_PATH" 2>/dev/null || stat -c%s "$IPA_PATH")
IPA_SIZE_MB=$((IPA_SIZE / 1024 / 1024))
echo "📦 IPA size: ${IPA_SIZE_MB}MB"
if [ "$IPA_SIZE" -lt 10485760 ]; then
echo "⚠️ WARNING: IPA file is suspiciously small (< 10MB)"
echo "This may indicate a build problem."
fi
echo "✅ iOS build output verification passed"
# Version updates moved to separate job to avoid race conditions
- name: Remove project.pbxproj updates we don't want to commit
@@ -624,8 +788,8 @@ jobs:
echo "Node modules: $NODE_SIZE"
fi
if [ -d "${{ env.APP_PATH }}/ios/vendor/bundle" ]; then
GEMS_SIZE=$(du -sh "${{ env.APP_PATH }}/ios/vendor/bundle" | cut -f1)
if [ -d "${{ env.APP_PATH }}/vendor/bundle" ]; then
GEMS_SIZE=$(du -sh "${{ env.APP_PATH }}/vendor/bundle" | cut -f1)
echo "Ruby gems: $GEMS_SIZE"
fi
@@ -638,6 +802,7 @@ jobs:
echo "💡 GitHub Actions cache limit: 10GB per repository"
build-android:
needs: [bump-version]
runs-on: ubuntu-latest
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
@@ -664,9 +829,8 @@ jobs:
if: inputs.platform != 'ios'
with:
fetch-depth: 0
# When triggered by PR merge, use the merge commit on staging
# This ensures we deploy exactly what landed on staging (including version.json from source + any conflict resolutions)
ref: ${{ github.event.pull_request.merge_commit_sha || 'staging' }}
# Checkout the branch that triggered the workflow
ref: ${{ github.ref_name }}
- uses: "google-github-actions/auth@v2"
with:
project_id: "plucky-tempo-454713-r0"
@@ -743,26 +907,6 @@ jobs:
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Determine version bump from PR labels or input
id: version-bump
run: |
VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
# Override with PR label if present
if [ "${{ github.event_name }}" = "pull_request" ]; then
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q "version:major"; then
VERSION_BUMP="major"
elif echo "$LABELS" | grep -q "version:minor"; then
VERSION_BUMP="minor"
elif echo "$LABELS" | grep -q "version:patch"; then
VERSION_BUMP="patch"
fi
fi
echo "version_bump=$VERSION_BUMP" >> $GITHUB_OUTPUT
echo "📦 Version bump type: $VERSION_BUMP"
- name: Verify branch and commit (Android)
if: inputs.platform != 'ios'
run: |
@@ -770,17 +914,33 @@ jobs:
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
echo "Current commit: $(git rev-parse HEAD)"
echo "Current commit message: $(git log -1 --pretty=format:'%s')"
echo "Staging HEAD commit: $(git rev-parse origin/staging)"
echo "Staging HEAD message: $(git log -1 --pretty=format:'%s' origin/staging)"
BUILD_BRANCH="${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}"
echo "Building from branch: $BUILD_BRANCH"
echo "Target HEAD commit: $(git rev-parse origin/$BUILD_BRANCH)"
echo "Target HEAD message: $(git log -1 --pretty=format:'%s' origin/$BUILD_BRANCH)"
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/staging)" ]; then
echo "⚠️ WARNING: Current commit differs from latest staging commit"
echo "This might indicate we're not building from the latest staging branch"
git log --oneline HEAD..origin/staging || true
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/$BUILD_BRANCH)" ]; then
echo "⚠️ WARNING: Current commit differs from latest $BUILD_BRANCH commit"
echo "This might indicate we're not building from the latest $BUILD_BRANCH branch"
git log --oneline HEAD..origin/$BUILD_BRANCH || true
else
echo "✅ Building from latest staging commit"
echo "✅ Building from latest $BUILD_BRANCH commit"
fi
- name: Apply version bump for build
if: inputs.platform != 'ios'
run: |
cd ${{ env.APP_PATH }}
VERSION="${{ needs.bump-version.outputs.version }}"
IOS_BUILD="${{ needs.bump-version.outputs.ios_build }}"
ANDROID_BUILD="${{ needs.bump-version.outputs.android_build }}"
echo "📝 Applying version bump for Android build: $VERSION (iOS Build: $IOS_BUILD, Android Build: $ANDROID_BUILD)"
# Use version-manager script to apply versions
node ${{ env.APP_PATH }}/scripts/version-manager.cjs apply "$VERSION" "$IOS_BUILD" "$ANDROID_BUILD"
- name: Cache Yarn artifacts
id: yarn-cache
uses: ./.github/actions/cache-yarn
@@ -795,7 +955,7 @@ jobs:
id: gems-cache
uses: ./.github/actions/cache-bundler
with:
path: ${{ env.APP_PATH }}/ios/vendor/bundle
path: ${{ env.APP_PATH }}/vendor/bundle
lock-file: app/Gemfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
@@ -843,8 +1003,8 @@ jobs:
echo "✅ Lock files exist"
- name: Install Mobile Dependencies
if: inputs.platform != 'ios'
- name: Install Mobile Dependencies (main repo)
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
uses: ./.github/actions/mobile-setup
with:
app_path: ${{ env.APP_PATH }}
@@ -855,6 +1015,17 @@ jobs:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
PLATFORM: ${{ inputs.platform }}
- name: Install Mobile Dependencies (forked PRs - no secrets)
if: inputs.platform != 'ios' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
uses: ./.github/actions/mobile-setup
with:
app_path: ${{ env.APP_PATH }}
node_version: ${{ env.NODE_VERSION }}
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
env:
PLATFORM: ${{ inputs.platform }}
# android specific steps
- name: Setup Android SDK
@@ -938,6 +1109,9 @@ jobs:
- name: Build AAB with Fastlane
if: inputs.platform != 'ios'
env:
CI_VERSION: ${{ needs.bump-version.outputs.version }}
CI_IOS_BUILD: ${{ needs.bump-version.outputs.ios_build }}
CI_ANDROID_BUILD: ${{ needs.bump-version.outputs.android_build }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
@@ -957,20 +1131,53 @@ jobs:
# Determine deployment track and version bump
DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}"
VERSION_BUMP="${{ steps.version-bump.outputs.version_bump }}"
VERSION_BUMP="${{ needs.bump-version.outputs.version_bump_type }}"
TEST_MODE="${{ inputs.test_mode || false }}"
echo "🤖 Build Configuration:"
echo " - Track: $DEPLOYMENT_TRACK"
echo " - Version Bump: $VERSION_BUMP"
echo " - Version Bump: $VERSION_BUMP (already applied in bump-version job)"
echo " - Version: ${{ needs.bump-version.outputs.version }}"
echo " - Android Build: ${{ needs.bump-version.outputs.android_build }}"
echo " - Test Mode: $TEST_MODE"
echo "🔨 Building AAB with Fastlane..."
bundle exec fastlane android build_only \
deployment_track:$DEPLOYMENT_TRACK \
version_bump:$VERSION_BUMP \
version_bump:skip \
--verbose
- name: Verify Android build output
if: inputs.platform != 'ios'
run: |
cd ${{ env.APP_PATH }}
echo "🔍 Verifying Android build artifacts..."
# Check for AAB file
AAB_PATH="android/app/build/outputs/bundle/release/app-release.aab"
if [ ! -f "$AAB_PATH" ]; then
echo "❌ ERROR: AAB file not found at $AAB_PATH"
echo "Build may have failed silently. Check Fastlane logs above."
exit 1
fi
echo "✅ Found AAB: $AAB_PATH"
# Check file size (should be at least 5MB for a real app)
AAB_SIZE=$(stat -c%s "$AAB_PATH" 2>/dev/null || stat -f%z "$AAB_PATH")
AAB_SIZE_MB=$((AAB_SIZE / 1024 / 1024))
echo "📦 AAB size: ${AAB_SIZE_MB}MB"
if [ "$AAB_SIZE" -lt 5242880 ]; then
echo "⚠️ WARNING: AAB file is suspiciously small (< 5MB)"
echo "This may indicate a build problem."
fi
echo "✅ Android build output verification passed"
- name: Upload to Google Play Store using WIF
if: inputs.platform != 'ios' && inputs.test_mode != true
timeout-minutes: 10
@@ -1016,9 +1223,12 @@ jobs:
echo "💡 GitHub Actions cache limit: 10GB per repository"
# Consolidated version bump PR - runs after both platforms complete
# NOTE: This job checks out the TARGET branch for version bump (default: dev)
# This is DIFFERENT from the build branch - we build from staging/feature branch,
# but create the version bump PR to dev so it can be reviewed before merging to staging
create-version-bump-pr:
runs-on: ubuntu-latest
needs: [build-ios, build-android]
needs: [bump-version, build-ios, build-android]
if: |
always() &&
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
@@ -1029,46 +1239,145 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Checkout staging where the builds ran
ref: ${{ github.event.pull_request.merge_commit_sha || 'staging' }}
# Checkout target branch for version bump PR (default: dev, override with bump_target_branch input)
ref: ${{ inputs.bump_target_branch || 'dev' }}
- name: Get version from package.json
id: get-version
uses: ./.github/actions/get-version
- name: Setup Node.js
uses: actions/setup-node@v4
with:
app_path: ${{ env.APP_PATH }}
node-version-file: .nvmrc
- name: Determine platforms that succeeded
- name: Apply version bump from outputs
run: |
cd ${{ env.APP_PATH }}
VERSION="${{ needs.bump-version.outputs.version }}"
IOS_BUILD="${{ needs.bump-version.outputs.ios_build }}"
ANDROID_BUILD="${{ needs.bump-version.outputs.android_build }}"
echo "📝 Applying version bump: $VERSION (iOS: $IOS_BUILD, Android: $ANDROID_BUILD)"
# Update package.json version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '$VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
console.log('✅ Updated package.json');
"
# Update version.json build numbers
node -e "
const fs = require('fs');
const version = JSON.parse(fs.readFileSync('version.json', 'utf8'));
version.ios.build = $IOS_BUILD;
version.android.build = $ANDROID_BUILD;
fs.writeFileSync('version.json', JSON.stringify(version, null, 2) + '\n');
console.log('✅ Updated version.json');
"
echo "✅ Versions applied successfully"
- name: Determine platforms that succeeded and PR title
id: platforms
run: |
PLATFORMS=""
if [ "${{ needs.build-ios.result }}" = "success" ]; then
PLATFORMS="${PLATFORMS}iOS "
fi
if [ "${{ needs.build-android.result }}" = "success" ]; then
PLATFORMS="${PLATFORMS}Android"
fi
echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT
echo "📱 Successful builds: $PLATFORMS"
VERSION="${{ needs.bump-version.outputs.version }}"
PLATFORM="${{ needs.bump-version.outputs.platform }}"
IOS_RESULT="${{ needs.build-ios.result }}"
ANDROID_RESULT="${{ needs.build-android.result }}"
- name: Create consolidated version bump PR
uses: ./.github/actions/create-version-bump-pr
with:
platform: mobile
version: ${{ steps.get-version.outputs.version }}
file_paths: |
app/version.json
app/package.json
app/ios/Self.xcodeproj/project.pbxproj
app/ios/OpenPassport/Info.plist
app/android/app/build.gradle
github_token: ${{ secrets.GITHUB_TOKEN }}
# Determine what was actually built
PLATFORMS_BUILT=""
if [ "$IOS_RESULT" = "success" ]; then
PLATFORMS_BUILT="iOS"
fi
if [ "$ANDROID_RESULT" = "success" ]; then
if [ -n "$PLATFORMS_BUILT" ]; then
PLATFORMS_BUILT="${PLATFORMS_BUILT} & Android"
else
PLATFORMS_BUILT="Android"
fi
fi
# Generate PR title based on what was bumped
if [ "$PLATFORM" = "ios" ]; then
PR_TITLE="chore: bump iOS version to $VERSION"
elif [ "$PLATFORM" = "android" ]; then
PR_TITLE="chore: bump Android version to $VERSION"
else
PR_TITLE="chore: bump mobile app version to $VERSION"
fi
echo "platforms=${PLATFORMS_BUILT}" >> $GITHUB_OUTPUT
echo "pr_title=${PR_TITLE}" >> $GITHUB_OUTPUT
echo "📱 Successful builds: $PLATFORMS_BUILT"
echo "📝 PR title: $PR_TITLE"
- name: Create version bump PR
if: inputs.dry_run != true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.bump-version.outputs.version }}"
TARGET_BRANCH="${{ inputs.bump_target_branch || 'dev' }}"
# Use version-based branch name for idempotency
BRANCH_NAME="ci/bump-mobile-version-${VERSION}"
PR_TITLE="${{ steps.platforms.outputs.pr_title }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if branch already exists (idempotent PR creation)
if git ls-remote --heads origin "${BRANCH_NAME}" | grep -q "${BRANCH_NAME}"; then
echo "⚠️ Branch ${BRANCH_NAME} already exists"
echo " Version bump PR may already exist for version ${VERSION}"
echo " Skipping PR creation to avoid duplicates"
exit 0
fi
# Commit the version changes
cd ${{ env.APP_PATH }}
git add package.json version.json
if git diff --cached --quiet; then
echo "⚠️ No version changes to commit"
exit 0
fi
git commit -m "chore: bump mobile app version to $VERSION [skip ci]"
# Create new branch from current HEAD (bump target branch with version bump)
git checkout -b ${BRANCH_NAME}
# Push the branch
git push --set-upstream origin ${BRANCH_NAME}
# Create PR to target branch (usually dev)
echo "Creating PR to ${TARGET_BRANCH}..."
gh pr create \
--base ${TARGET_BRANCH} \
--head ${BRANCH_NAME} \
--title "${PR_TITLE}" \
--body "🤖 Automated version bump after successful deployment
**Version:** $VERSION
**iOS Build:** ${{ needs.bump-version.outputs.ios_build }}
**Android Build:** ${{ needs.bump-version.outputs.android_build }}
**Platforms Built:** ${{ steps.platforms.outputs.platforms }}
**Build Branch:** ${{ github.ref_name }}
**Target Branch:** ${TARGET_BRANCH}
This PR was automatically created by the mobile deployment workflow." \
--label "automated"
echo "✅ Version bump PR created successfully to ${TARGET_BRANCH}"
# Create git tags after successful deployment
create-release-tags:
needs: [build-ios, build-android, create-version-bump-pr]
needs: [bump-version, build-ios, build-android, create-version-bump-pr]
if: |
always() &&
(inputs.dry_run != true) &&
needs.create-version-bump-pr.result == 'success' &&
(needs.build-ios.result == 'success' || needs.build-android.result == 'success') &&
(inputs.deployment_track == 'production')
@@ -1077,9 +1386,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# When triggered by PR merge, use the merge commit on staging
# This ensures we tag exactly what landed on staging (including version.json from source + any conflict resolutions)
ref: ${{ github.event.pull_request.merge_commit_sha || 'staging' }}
# Checkout target branch for tagging (usually dev)
ref: ${{ inputs.bump_target_branch || 'dev' }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
@@ -1091,47 +1399,75 @@ jobs:
run: |
cd ${{ env.APP_PATH }}
# Read current version info
VERSION=$(cat package.json | jq -r .version)
IOS_BUILD=$(cat version.json | jq -r .ios.build)
ANDROID_BUILD=$(cat version.json | jq -r .android.build)
# Use version info from bump-version outputs
VERSION="${{ needs.bump-version.outputs.version }}"
IOS_BUILD="${{ needs.bump-version.outputs.ios_build }}"
ANDROID_BUILD="${{ needs.bump-version.outputs.android_build }}"
echo "📦 Creating tags for version $VERSION"
# Create main version tag
if ! git tag -l | grep -q "^v${VERSION}$"; then
git tag -a "v${VERSION}" -m "Release ${VERSION}"
# Create main version tag (idempotent)
if git tag -a "v${VERSION}" -m "Release ${VERSION}" 2>/dev/null; then
echo "✅ Created tag: v${VERSION}"
else
echo "⏭️ Tag v${VERSION} already exists"
EXIT_CODE=$?
if [ $EXIT_CODE -eq 128 ]; then
echo "⏭️ Tag v${VERSION} already exists"
else
echo "❌ Failed to create tag v${VERSION} with exit code $EXIT_CODE"
exit 1
fi
fi
# Create platform-specific tags if deployments succeeded
# Create platform-specific tags if deployments succeeded (idempotent)
if [ "${{ needs.build-ios.result }}" = "success" ]; then
TAG_NAME="v${VERSION}-ios-${IOS_BUILD}"
if ! git tag -l | grep -q "^${TAG_NAME}$"; then
git tag -a "${TAG_NAME}" -m "iOS Release ${VERSION} (Build ${IOS_BUILD})"
if git tag -a "${TAG_NAME}" -m "iOS Release ${VERSION} (Build ${IOS_BUILD})" 2>/dev/null; then
echo "✅ Created tag: ${TAG_NAME}"
else
EXIT_CODE=$?
if [ $EXIT_CODE -eq 128 ]; then
echo "⏭️ Tag ${TAG_NAME} already exists"
else
echo "❌ Failed to create tag ${TAG_NAME} with exit code $EXIT_CODE"
exit 1
fi
fi
fi
if [ "${{ needs.build-android.result }}" = "success" ]; then
TAG_NAME="v${VERSION}-android-${ANDROID_BUILD}"
if ! git tag -l | grep -q "^${TAG_NAME}$"; then
git tag -a "${TAG_NAME}" -m "Android Release ${VERSION} (Build ${ANDROID_BUILD})"
if git tag -a "${TAG_NAME}" -m "Android Release ${VERSION} (Build ${ANDROID_BUILD})" 2>/dev/null; then
echo "✅ Created tag: ${TAG_NAME}"
else
EXIT_CODE=$?
if [ $EXIT_CODE -eq 128 ]; then
echo "⏭️ Tag ${TAG_NAME} already exists"
else
echo "❌ Failed to create tag ${TAG_NAME} with exit code $EXIT_CODE"
exit 1
fi
fi
fi
# Push all tags
git push origin --tags
echo "🚀 Tags pushed to repository"
# Push all tags (force to handle any conflicts)
if git push origin --tags 2>/dev/null; then
echo "🚀 Tags pushed to repository"
else
echo "⚠️ Some tags may already exist on remote, trying force push..."
git push origin --tags --force
echo "🚀 Tags force-pushed to repository"
fi
- name: Generate changelog for release
id: changelog
run: |
cd ${{ env.APP_PATH }}
VERSION="${{ needs.bump-version.outputs.version }}"
IOS_BUILD="${{ needs.bump-version.outputs.ios_build }}"
ANDROID_BUILD="${{ needs.bump-version.outputs.android_build }}"
# Find the previous version tag
PREV_TAG=$(git tag -l "v*" | grep -v "-" | sort -V | tail -2 | head -1 || echo "")

View File

@@ -34,7 +34,7 @@ jobs:
concurrency:
group: ${{ github.workflow }}-android-${{ github.ref }}
cancel-in-progress: true
timeout-minutes: 60
timeout-minutes: 120
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -178,7 +178,7 @@ jobs:
if-no-files-found: warn
e2e-ios:
timeout-minutes: 60
timeout-minutes: 120
runs-on: macos-latest-large
concurrency:
group: ${{ github.workflow }}-ios-${{ github.ref }}

View File

@@ -95,27 +95,14 @@ platform :ios do
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# Handle version bumping
if !test_mode
require_relative "helpers/version_manager"
case version_bump
when "major", "minor", "patch"
# Use Node.js with semver to bump version
sh("cd .. && node -e \"const fs = require('fs'); const pkg = require('./package.json'); const semver = require('semver'); pkg.version = semver.inc(pkg.version, '#{version_bump}'); fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\\n');\"")
UI.success("✅ Bumped #{version_bump} version")
# Sync the new version to iOS project files
sync_version
UI.success("✅ Synced MARKETING_VERSION to iOS project")
when "build"
# Build number is handled in prepare_ios_build
UI.message("📦 Build number will be incremented during build")
end
# In CI, version should already be set by version-manager.cjs
# Verify it matches expected values
if is_ci && version_bump == "skip"
Fastlane::Helpers.verify_ci_version_match
end
# Prepare and build
result = prepare_ios_build(prod_release: deployment_track == "production")
# Prepare and build (no version bumping inside)
result = prepare_ios_build(prod_release: deployment_track == "production", version_bump: version_bump)
# Handle deployment based on track
if test_mode
@@ -146,6 +133,8 @@ platform :ios do
end
private_lane :prepare_ios_build do |options|
version_bump = options[:version_bump] || "build"
if local_development
# app breaks with Xcode 16.3
xcode_select "/Applications/Xcode.app"
@@ -183,10 +172,11 @@ platform :ios do
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get build number from version.json and increment it
build_number = Fastlane::Helpers.bump_ios_build_number
# Read build number from version.json (already set by CI or local version-manager.cjs)
build_number = Fastlane::Helpers.get_ios_build_number
UI.message("📦 Using iOS build number: #{build_number}")
# Update Xcode project with new build number
# Update Xcode project with build number
increment_build_number(
build_number: build_number,
xcodeproj: "ios/#{ENV["IOS_PROJECT_NAME"]}.xcodeproj",
@@ -234,7 +224,7 @@ platform :ios do
workspace: "#{workspace_path}",
scheme: PROJECT_SCHEME,
export_method: "app-store",
output_directory: "build",
output_directory: "ios/build",
clean: true,
export_options: {
method: "app-store",
@@ -298,37 +288,24 @@ platform :android do
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# Handle version bumping
if !test_mode
require_relative "helpers/version_manager"
case version_bump
when "major", "minor", "patch"
# Use Node.js with semver to bump version
sh("cd .. && node -e \"const fs = require('fs'); const pkg = require('./package.json'); const semver = require('semver'); pkg.version = semver.inc(pkg.version, '#{version_bump}'); fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\\n');\"")
UI.success("✅ Bumped #{version_bump} version")
# Get the new version and sync to build.gradle
new_version = Fastlane::Helpers::VersionManager.get_current_version
android_set_version_name(
version_name: new_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
when "build"
# Build number is automatically incremented during build
UI.message("📦 Build number will be incremented")
end
# In CI, version should already be set by version-manager.cjs
# Verify it matches expected values
if is_ci && version_bump == "skip"
Fastlane::Helpers.verify_ci_version_match
end
# Map deployment track to Play Store track
play_store_track = deployment_track == "production" ? "production" : "internal"
# Build and deploy
upload_android_build(track: play_store_track, test_mode: test_mode, deployment_track: deployment_track)
upload_android_build(track: play_store_track, test_mode: test_mode, deployment_track: deployment_track, version_bump: version_bump)
end
private_lane :upload_android_build do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
version_bump = options[:version_bump] || "build"
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
@@ -352,10 +329,11 @@ platform :android do
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get version code from version.json and increment it
version_code = Fastlane::Helpers.bump_android_build_number
# Read version code from version.json (already set by CI or local version-manager.cjs)
version_code = Fastlane::Helpers.get_android_build_number
UI.message("📦 Using Android build number: #{version_code}")
# Update build.gradle with new version code
# Update build.gradle with version code
increment_version_code(
version_code: version_code,
gradle_file_path: android_gradle_file_path.gsub("../", ""),

View File

@@ -44,22 +44,37 @@ module Fastlane
data["android"]["build"]
end
def bump_ios_build_number
data = read_version_file
current = data["ios"]["build"]
data["ios"]["build"] = current + 1
write_version_file(data)
UI.success("iOS build number bumped from #{current} to #{data["ios"]["build"]}")
data["ios"]["build"]
end
def verify_ci_version_match
# Verify that versions were pre-set by CI
unless ENV["CI_VERSION"] && ENV["CI_IOS_BUILD"] && ENV["CI_ANDROID_BUILD"]
UI.user_error!("CI must set CI_VERSION, CI_IOS_BUILD, and CI_ANDROID_BUILD environment variables")
end
def bump_android_build_number
data = read_version_file
current = data["android"]["build"]
data["android"]["build"] = current + 1
write_version_file(data)
UI.success("Android build number bumped from #{current} to #{data["android"]["build"]}")
data["android"]["build"]
pkg_version = get_current_version
ios_build = get_ios_build_number
android_build = get_android_build_number
expected_version = ENV["CI_VERSION"]
expected_ios_build = ENV["CI_IOS_BUILD"].to_i
expected_android_build = ENV["CI_ANDROID_BUILD"].to_i
version_matches = pkg_version == expected_version
ios_matches = ios_build == expected_ios_build
android_matches = android_build == expected_android_build
unless version_matches && ios_matches && android_matches
UI.error("Version mismatch detected!")
UI.error("Expected: v#{expected_version} (iOS: #{expected_ios_build}, Android: #{expected_android_build})")
UI.error("Actual: v#{pkg_version} (iOS: #{ios_build}, Android: #{android_build})")
UI.user_error!("Version mismatch! CI version-manager script should have set these correctly.")
end
UI.success("✅ Version verification passed:")
UI.message(" Version: #{pkg_version}")
UI.message(" iOS Build: #{ios_build}")
UI.message(" Android Build: #{android_build}")
{ version: pkg_version, ios_build: ios_build, android_build: android_build }
end
def update_deployment_timestamp(platform)

329
app/scripts/version-manager.cjs Executable file
View File

@@ -0,0 +1,329 @@
#!/usr/bin/env node
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Centralized Version Manager for Mobile Deployments
*
* Single source of truth for all version operations across:
* - GitHub Actions workflows
* - Fastlane (read-only consumption)
* - Local development
*
* Version Bump Behavior (Option B - Continue build numbers):
* - major: 2.6.9 → 3.0.0, increment build numbers
* - minor: 2.6.9 → 2.7.0, increment build numbers
* - patch: 2.6.9 → 2.6.10, increment build numbers
* - build: 2.6.9 → 2.6.9, increment build numbers only
*
* Platform-specific logic:
* - ios: Only increment iOS build number
* - android: Only increment Android build number
* - both/undefined: Increment both build numbers
*/
const fs = require('fs');
const path = require('path');
const APP_DIR = path.resolve(__dirname, '..');
const PACKAGE_JSON_PATH = path.join(APP_DIR, 'package.json');
const VERSION_JSON_PATH = path.join(APP_DIR, 'version.json');
/**
* Read package.json
*/
function readPackageJson() {
if (!fs.existsSync(PACKAGE_JSON_PATH)) {
throw new Error(`package.json not found at ${PACKAGE_JSON_PATH}`);
}
try {
return JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
} catch (error) {
throw new Error(`Failed to parse package.json: ${error.message}`);
}
}
/**
* Read version.json
*/
function readVersionJson() {
if (!fs.existsSync(VERSION_JSON_PATH)) {
throw new Error(`version.json not found at ${VERSION_JSON_PATH}`);
}
try {
return JSON.parse(fs.readFileSync(VERSION_JSON_PATH, 'utf8'));
} catch (error) {
throw new Error(`Failed to parse version.json: ${error.message}`);
}
}
/**
* Write package.json
*/
function writePackageJson(data) {
try {
fs.writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(data, null, 2) + '\n');
} catch (error) {
throw new Error(`Failed to write package.json: ${error.message}`);
}
}
/**
* Write version.json
*/
function writeVersionJson(data) {
try {
fs.writeFileSync(VERSION_JSON_PATH, JSON.stringify(data, null, 2) + '\n');
} catch (error) {
throw new Error(`Failed to write version.json: ${error.message}`);
}
}
/**
* Get current version information
*/
function getVersionInfo() {
const pkg = readPackageJson();
const versionData = readVersionJson();
return {
version: pkg.version,
iosBuild: versionData.ios.build,
androidBuild: versionData.android.build,
iosLastDeployed: versionData.ios.lastDeployed,
androidLastDeployed: versionData.android.lastDeployed,
};
}
/**
* Bump semantic version (major/minor/patch)
*/
function bumpSemanticVersion(currentVersion, bumpType) {
const parts = currentVersion.split('.').map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
throw new Error(
`Invalid version format: ${currentVersion}. Expected X.Y.Z`,
);
}
let [major, minor, patch] = parts;
switch (bumpType) {
case 'major':
major += 1;
minor = 0;
patch = 0;
break;
case 'minor':
minor += 1;
patch = 0;
break;
case 'patch':
patch += 1;
break;
default:
throw new Error(
`Invalid bump type: ${bumpType}. Expected major, minor, or patch`,
);
}
return `${major}.${minor}.${patch}`;
}
/**
* Bump version and build numbers
*
* @param {string} bumpType - 'major', 'minor', 'patch', or 'build'
* @param {string} platform - 'ios', 'android', or 'both' (default)
* @returns {object} - New version info
*/
function bumpVersion(bumpType, platform = 'both') {
const validBumpTypes = ['major', 'minor', 'patch', 'build'];
const validPlatforms = ['ios', 'android', 'both'];
if (!validBumpTypes.includes(bumpType)) {
throw new Error(
`Invalid bump type: ${bumpType}. Expected: ${validBumpTypes.join(', ')}`,
);
}
if (!validPlatforms.includes(platform)) {
throw new Error(
`Invalid platform: ${platform}. Expected: ${validPlatforms.join(', ')}`,
);
}
const pkg = readPackageJson();
const versionData = readVersionJson();
let newVersion = pkg.version;
// Bump semantic version if major/minor/patch
if (bumpType !== 'build') {
newVersion = bumpSemanticVersion(pkg.version, bumpType);
console.log(
`📦 Bumping ${bumpType} version: ${pkg.version}${newVersion}`,
);
} else {
console.log(`📦 Keeping version: ${newVersion} (build-only bump)`);
}
// Bump build numbers based on platform
let newIosBuild = versionData.ios.build;
let newAndroidBuild = versionData.android.build;
if (platform === 'ios' || platform === 'both') {
newIosBuild += 1;
console.log(`🍎 iOS build: ${versionData.ios.build}${newIosBuild}`);
} else {
console.log(`🍎 iOS build: ${newIosBuild} (unchanged)`);
}
if (platform === 'android' || platform === 'both') {
newAndroidBuild += 1;
console.log(
`🤖 Android build: ${versionData.android.build}${newAndroidBuild}`,
);
} else {
console.log(`🤖 Android build: ${newAndroidBuild} (unchanged)`);
}
return {
version: newVersion,
iosBuild: newIosBuild,
androidBuild: newAndroidBuild,
};
}
/**
* Apply version changes to files
*/
function applyVersions(version, iosBuild, androidBuild) {
console.log(`📝 Applying versions to files...`);
console.log(` Version: ${version}`);
console.log(` iOS Build: ${iosBuild}`);
console.log(` Android Build: ${androidBuild}`);
// Update package.json
const pkg = readPackageJson();
pkg.version = version;
writePackageJson(pkg);
console.log(`✅ Updated package.json`);
// Update version.json
const versionData = readVersionJson();
versionData.ios.build = iosBuild;
versionData.android.build = androidBuild;
writeVersionJson(versionData);
console.log(`✅ Updated version.json`);
}
/**
* CLI Interface
*/
function main() {
const args = process.argv.slice(2);
const command = args[0];
try {
switch (command) {
case 'get': {
// Get current version info
const info = getVersionInfo();
console.log(JSON.stringify(info, null, 2));
// Also output for GitHub Actions
if (process.env.GITHUB_OUTPUT) {
const output = [
`version=${info.version}`,
`ios_build=${info.iosBuild}`,
`android_build=${info.androidBuild}`,
].join('\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, output + '\n');
}
break;
}
case 'bump': {
// Bump version: bump <type> <platform>
const bumpType = args[1] || 'build';
const platform = args[2] || 'both';
const result = bumpVersion(bumpType, platform);
console.log(`\n✅ Version bump calculated:`);
console.log(JSON.stringify(result, null, 2));
// Output for GitHub Actions
if (process.env.GITHUB_OUTPUT) {
const output = [
`version=${result.version}`,
`ios_build=${result.iosBuild}`,
`android_build=${result.androidBuild}`,
].join('\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, output + '\n');
}
break;
}
case 'apply': {
// Apply version: apply <version> <iosBuild> <androidBuild>
const version = args[1];
const iosBuild = parseInt(args[2], 10);
const androidBuild = parseInt(args[3], 10);
if (!version || isNaN(iosBuild) || isNaN(androidBuild)) {
throw new Error('Usage: apply <version> <iosBuild> <androidBuild>');
}
applyVersions(version, iosBuild, androidBuild);
console.log(`\n✅ Versions applied successfully`);
break;
}
default:
console.log(`
Mobile Version Manager
Usage:
node version-manager.cjs <command> [options]
Commands:
get Get current version information
bump <type> <platform> Bump version and calculate new build numbers
type: major|minor|patch|build (default: build)
platform: ios|android|both (default: both)
apply <version> <ios> <android> Apply specific version and build numbers
Examples:
node version-manager.cjs get
node version-manager.cjs bump build both
node version-manager.cjs bump patch ios
node version-manager.cjs apply 2.7.0 180 109
`);
process.exit(command ? 1 : 0);
}
} catch (error) {
console.error(`❌ Error: ${error.message}`);
process.exit(1);
}
}
// Run CLI if called directly
if (require.main === module) {
main();
}
// Export functions for use as module
module.exports = {
applyVersions,
bumpVersion,
getVersionInfo,
readPackageJson,
readVersionJson,
};