diff --git a/.github/actions/create-version-bump-pr/action.yml b/.github/actions/create-version-bump-pr/action.yml index 24de7b333..2a23ce23b 100644 --- a/.github/actions/create-version-bump-pr/action.yml +++ b/.github/actions/create-version-bump-pr/action.yml @@ -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." - exit 0 - fi + # Ensure we're on staging branch, not detached HEAD + git fetch origin staging dev + git checkout staging - # Commit changes on temporary branch - git checkout -b temp-version-commit - git commit -m "chore: bump ${{ inputs.platform }} version for ${{ inputs.version }} [skip ci]" + # Check if staging has commits not in dev (version bumps + any build changes) + COMMITS_AHEAD=$(git rev-list --count origin/dev..staging) - # Create new branch from dev - git fetch origin dev - git checkout -b ${BRANCH_NAME} origin/dev + if [ "$COMMITS_AHEAD" -eq 0 ]; then + echo "โ„น๏ธ No new commits on staging compared to dev. Skipping PR creation." + exit 0 + fi - # Cherry-pick only the version changes - git cherry-pick temp-version-commit + echo "๐Ÿ“Š Staging is $COMMITS_AHEAD commit(s) ahead of dev" - # Clean up temporary branch - git branch -D temp-version-commit + # Create new branch from current staging (which has all version changes) + git checkout -b ${BRANCH_NAME} - # 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 }} diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml index 7a3ab1516..0c183f7ca 100644 --- a/.github/actions/get-version/action.yml +++ b/.github/actions/get-version/action.yml @@ -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 diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 0cd5f14b1..70ac4b95c 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -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)" - - 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 + 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/$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: Apply version bump from outputs + run: | + cd ${{ env.APP_PATH }} - - name: Determine platforms that succeeded + 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 " + VERSION="${{ needs.bump-version.outputs.version }}" + PLATFORM="${{ needs.bump-version.outputs.platform }}" + IOS_RESULT="${{ needs.build-ios.result }}" + ANDROID_RESULT="${{ needs.build-android.result }}" + + # Determine what was actually built + PLATFORMS_BUILT="" + if [ "$IOS_RESULT" = "success" ]; then + PLATFORMS_BUILT="iOS" fi - if [ "${{ needs.build-android.result }}" = "success" ]; then - PLATFORMS="${PLATFORMS}Android" + if [ "$ANDROID_RESULT" = "success" ]; then + if [ -n "$PLATFORMS_BUILT" ]; then + PLATFORMS_BUILT="${PLATFORMS_BUILT} & Android" + else + PLATFORMS_BUILT="Android" + fi fi - echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT - echo "๐Ÿ“ฑ Successful builds: $PLATFORMS" - - 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 }} + # 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 "") diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index 9b87d5c1a..2a5545c80 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -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 }} diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index cf16250c2..aff78d7b5 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -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("../", ""), diff --git a/app/fastlane/helpers/version_manager.rb b/app/fastlane/helpers/version_manager.rb index 9322cf578..5c4f9d6d3 100644 --- a/app/fastlane/helpers/version_manager.rb +++ b/app/fastlane/helpers/version_manager.rb @@ -44,22 +44,37 @@ def get_android_build_number 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) diff --git a/app/scripts/version-manager.cjs b/app/scripts/version-manager.cjs new file mode 100755 index 000000000..a3317d6bc --- /dev/null +++ b/app/scripts/version-manager.cjs @@ -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 + 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 + 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 '); + } + + applyVersions(version, iosBuild, androidBuild); + console.log(`\nโœ… Versions applied successfully`); + break; + } + + default: + console.log(` +Mobile Version Manager + +Usage: + node version-manager.cjs [options] + +Commands: + get Get current version information + bump Bump version and calculate new build numbers + type: major|minor|patch|build (default: build) + platform: ios|android|both (default: both) + apply 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, +}; diff --git a/app/version.json b/app/version.json index 6a8cc6cb8..fd428910f 100644 --- a/app/version.json +++ b/app/version.json @@ -1,7 +1,7 @@ { "ios": { - "build": 178, - "lastDeployed": "2025-09-30T16:35:10Z" + "build": 179, + "lastDeployed": "2025-10-07T05:58:42Z" }, "android": { "build": 108,