name: Mobile Deploy # Runs on: # 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 === # - deploy:skip: Skip deployment entirely (no version bump, no builds) # - version:major/minor/patch: Control version bump type # - deploy:ios/deploy:android: 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 RUBY_VERSION: 3.2 JAVA_VERSION: 17 ANDROID_API_LEVEL: 35 ANDROID_NDK_VERSION: 27.0.12077973 XCODE_VERSION: 16.4 # Cache versioning - increment these to bust caches when needed GH_CACHE_VERSION: v1 # Global cache version GH_YARN_CACHE_VERSION: v1 # Yarn-specific cache version GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version GH_PODS_CACHE_VERSION: v1 # CocoaPods cache version GH_GRADLE_CACHE_VERSION: v1 # Gradle cache version # Path configuration WORKSPACE: ${{ github.workspace }} APP_PATH: ${{ github.workspace }}/app # Certificate/keystore paths ANDROID_KEYSTORE_PATH: /android/app/upload-keystore.jks ANDROID_PLAY_STORE_JSON_KEY_PATH: /android/play-store-key.json IOS_DIST_CERT_PATH: /ios/certs/dist_cert.p12 IOS_CONNECT_API_KEY_PATH: /ios/certs/connect_api_key.p8 IOS_PROV_PROFILE_PROJ_PATH: /ios/certs/profile.mobileprovision IOS_PROV_PROFILE_DIRECTORY: "~/Library/MobileDevice/Provisioning\ Profiles/" permissions: contents: read on: workflow_dispatch: inputs: platform: description: "Select platform to build" required: true default: "both" type: choice options: - ios - android - both test_mode: description: "Test mode (skip upload to stores)" required: false type: boolean default: false deployment_track: description: "Deployment track (internal/production)" required: false type: choice default: "internal" options: - internal - production version_bump: description: "Version bump type" required: false type: choice default: "build" options: - build - 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] branches: [staging] paths: - "app/**" - "!app/**/*.md" - "!app/docs/**" - "packages/mobile-sdk-alpha/**" - ".github/workflows/mobile-deploy.yml" workflow_call: inputs: platform: type: string required: true deployment_track: type: string required: false default: "internal" version_bump: type: string required: false default: "build" auto_deploy: type: boolean required: false default: false test_mode: type: boolean required: false 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 permissions: contents: read if: | (github.event_name != 'pull_request' || github.event.pull_request.merged == true) && !contains(github.event.pull_request.labels.*.name, 'deploy:skip') 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 "deploy:ios"; then PLATFORM="ios" elif echo "$LABELS" | grep -q "deploy:android"; 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." - name: Verify bump outputs were set run: | VERSION="${{ steps.bump.outputs.version }}" IOS_BUILD="${{ steps.bump.outputs.ios_build }}" ANDROID_BUILD="${{ steps.bump.outputs.android_build }}" if [ -z "$VERSION" ] || [ -z "$IOS_BUILD" ] || [ -z "$ANDROID_BUILD" ]; then echo "❌ Version bump failed to set required outputs" echo "version='$VERSION', ios_build='$IOS_BUILD', android_build='$ANDROID_BUILD'" exit 1 fi echo "✅ All version outputs verified" build-ios: needs: [bump-version] runs-on: macos-latest-large permissions: contents: read actions: write if: | (github.event_name != 'pull_request' || github.event.pull_request.merged == true) && ( (inputs.platform == 'ios' || inputs.platform == 'both') || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'deploy:android')) ) steps: - name: Mobile deployment status run: | echo "🚀 Mobile deployment is enabled - proceeding with iOS build" echo "📱 Platform: ${{ inputs.platform || 'both' }}" echo "🎯 Track: ${{ inputs.deployment_track || 'internal' }}" echo "📦 Version bump: ${{ inputs.version_bump || 'build' }}" if [ "${{ github.event_name }}" = "pull_request" ]; then echo "🔀 Triggered by PR merge: #${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}" echo "👤 Merged by: ${{ github.event.pull_request.merged_by.login }}" if ${{ contains(github.event.pull_request.labels.*.name, 'deploy:ios') }}; then echo "🏷️ Label: deploy:ios (skipping Android)" fi fi - uses: actions/checkout@v4 with: fetch-depth: 0 # Checkout the branch that triggered the workflow ref: ${{ github.ref_name }} - name: Read and sanitize Node.js version shell: bash run: | if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then echo "❌ .nvmrc is missing or empty"; exit 1; fi VERSION="$(tr -d '\r\n' < .nvmrc)" VERSION="${VERSION#v}" if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then echo "Invalid .nvmrc content: '$VERSION'"; exit 1; fi echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" - name: Verify branch and commit (iOS) if: needs.bump-version.outputs.platform != 'android' run: | echo "🔍 Verifying we're building from the correct branch and commit..." 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')" 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/$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 $BUILD_BRANCH commit" fi - name: Apply version bump for build if: needs.bump-version.outputs.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 with: xcode-version: ${{ env.XCODE_VERSION }} - name: Configure Xcode path if: inputs.platform != 'android' run: | echo "🔧 Configuring Xcode path to fix iOS SDK issues..." # Fix for macOS 15 runner iOS SDK issues # See: https://github.com/actions/runner-images/issues/12758 sudo xcode-select --switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app echo "✅ Xcode path configured" # Verify Xcode setup echo "Xcode version:" xcodebuild -version echo "Xcode path:" xcode-select -p - name: Cache Yarn artifacts id: yarn-cache uses: ./.github/actions/cache-yarn with: path: | .yarn/cache .yarn/install-state.gz .yarn/unplugged cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }} - name: Cache Ruby gems id: gems-cache uses: ./.github/actions/cache-bundler with: path: ${{ env.APP_PATH }}/ios/vendor/bundle lock-file: app/Gemfile.lock cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} - name: Cache CocoaPods id: pods-cache uses: ./.github/actions/cache-pods with: path: | ${{ env.APP_PATH }}/ios/Pods ~/Library/Caches/CocoaPods lock-file: app/ios/Podfile.lock cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_PODS_CACHE_VERSION }} - name: Log cache status run: | echo "Cache hit results:" echo "- Yarn cache hit: ${{ steps.yarn-cache.outputs.cache-hit }}" echo "- Gems cache hit: ${{ steps.gems-cache.outputs.cache-hit }}" echo "- Pods cache hit: ${{ steps.pods-cache.outputs.cache-hit }}" - name: Disable Yarn hardened mode run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV - name: Verify lock files are up to date run: | echo "🔍 Checking if lock files are in sync with dependency files..." # For yarn workspaces, yarn.lock is at root if [ ! -f "${{ env.WORKSPACE }}/yarn.lock" ]; then echo "❌ ERROR: yarn.lock file is missing at workspace root!" echo "Run 'yarn install' at the repository root and commit the yarn.lock file." exit 1 fi # Gemfile.lock is in app directory if [ ! -f "${{ env.APP_PATH }}/Gemfile.lock" ]; then echo "❌ ERROR: Gemfile.lock file is missing!" echo "Run 'bundle install' in the app directory and commit the Gemfile.lock file." exit 1 fi echo "✅ Lock files exist" - name: Install Mobile Dependencies (main repo) if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) 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: SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} - name: Install Mobile Dependencies (forked PRs - no secrets) if: inputs.platform != 'android' && 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 }} - name: Verify iOS Secrets if: inputs.platform != 'android' run: | # Verify App Store Connect API Key exists and contains PEM header if [ -z "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" ]; then echo "❌ Error: App Store Connect API Key cannot be empty" exit 1 fi # Verify Issuer ID is in correct format (UUID) if ! echo "${{ secrets.IOS_CONNECT_ISSUER_ID }}" | grep -E "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" >/dev/null; then echo "❌ Error: Invalid App Store Connect Issuer ID format (should be UUID)" exit 1 fi # Verify Key ID is in correct format (alphanumeric) if ! echo "${{ secrets.IOS_CONNECT_KEY_ID }}" | grep -E "^[A-Z0-9]{10}$" >/dev/null; then echo "❌ Error: Invalid App Store Connect Key ID format" exit 1 fi # Verify P12 password is not empty and meets basic security requirements if [ -z "${{ secrets.IOS_P12_PASSWORD }}" ]; then echo "❌ Error: P12 password cannot be empty" exit 1 fi # Verify base64 secrets are not empty if [ -z "${{ secrets.IOS_DIST_CERT_BASE64 }}" ]; then echo "❌ Error: Distribution certificate cannot be empty" exit 1 fi if [ -z "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" ]; then echo "❌ Error: Provisioning profile cannot be empty" exit 1 fi echo "✅ All iOS secrets verified successfully!" - name: Decode certificate and profile if: inputs.platform != 'android' run: | mkdir -p "${{ env.APP_PATH }}$(dirname "${{ env.IOS_DIST_CERT_PATH }}")" echo "${{ secrets.IOS_DIST_CERT_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} echo "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }} echo "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }} # for debugging...which can take some time :( - name: Verify ios secret checksums if: false # for debugging run: | echo "SHA256 of dist_cert.p12:" shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} echo "SHA256 of profile.mobileprovision:" shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }} echo "SHA256 of connect_api_key.p8:" shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }} echo "Certificate file size:" ls -l ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} echo "SHA256 of password:" echo -n "${{ secrets.IOS_P12_PASSWORD }}" | shasum -a 256 echo "SHA256 of connect_api_key_base64:" echo -n "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | shasum -a 256 echo "Verifying certificate..." if openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info >/dev/null 2>&1 || openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info 2>&1 | grep -q "MAC:"; then echo "✅ Certificate verification successful (algorithm warning can be safely ignored)" else echo "❌ Certificate verification failed - please check certificate validity" exit 1 fi - name: Verify iOS certificate and environment if: inputs.platform != 'android' && !env.ACT run: | # Check if certificate directory exists if [ ! -d "${{ env.APP_PATH }}/ios/certs" ]; then echo "❌ Error: iOS certificates directory not found at ${{ env.APP_PATH }}/ios/certs" exit 1 fi # Check if certificate file exists if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" ]; then echo "❌ Error: Distribution certificate not found at ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" exit 1 fi # Check certificate file permissions CERT_PERMS=$(ls -l "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" | awk '{print $1}') if [ "$CERT_PERMS" != "-rw-r--r--" ]; then echo "❌ Error: Distribution certificate has incorrect permissions: $CERT_PERMS" echo "Expected: -rw-r--r--" exit 1 fi # Check certificate file size CERT_SIZE=$(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}") if [ "$CERT_SIZE" -lt 1000 ]; then echo "❌ Error: Distribution certificate file size ($CERT_SIZE bytes) is suspiciously small" exit 1 fi # Check if we can create a test keychain TEST_KEYCHAIN="test.keychain" if ! security create-keychain -p "" "$TEST_KEYCHAIN" >/dev/null 2>&1; then echo "❌ Error: Unable to create test keychain. Check permissions." exit 1 fi security delete-keychain "$TEST_KEYCHAIN" >/dev/null 2>&1 echo "✅ Certificate and environment verification passed!" - name: Install certificate if: inputs.platform != 'android' && !env.ACT run: | security create-keychain -p "" build.keychain >/dev/null 2>&1 security default-keychain -s build.keychain >/dev/null 2>&1 security unlock-keychain -p "" build.keychain >/dev/null 2>&1 security import ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -k build.keychain -P '${{ secrets.IOS_P12_PASSWORD }}' -T /usr/bin/codesign >/dev/null 2>&1 security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain >/dev/null 2>&1 - name: Install provisioning profile if: inputs.platform != 'android' && !env.ACT env: IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }} IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }} IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }} IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }} run: | # Verify file exists before proceeding echo "Checking for provisioning profile at: ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" ]; then echo "❌ Error: Provisioning profile not found at specified path." exit 1 fi echo "Provisioning profile found." # Print file details echo "Provisioning Profile File Details:" echo "--------------------------------" echo "File size: $(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}") bytes" echo "File permissions: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $1}')" echo "File owner: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $3}')" echo "--------------------------------" # Create a temporary plist file to extract UUID TEMP_PLIST_PATH=$(mktemp /tmp/profile_plist.XXXXXX) # Extract plist from mobileprovision file echo "Extracting plist from provisioning profile..." security cms -D -i "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" -o "$TEMP_PLIST_PATH" if [ $? -ne 0 ]; then echo "❌ Error: Failed to extract plist from provisioning profile" exit 1 fi # Extract UUID and profile name from plist echo "Extracting UUID and profile name from plist..." PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" "$TEMP_PLIST_PATH" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$PROFILE_UUID" ]; then echo "❌ Error: Failed to extract UUID from provisioning profile" cat "$TEMP_PLIST_PATH" | head -20 exit 1 fi # Extract the actual profile name from within the file PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$TEMP_PLIST_PATH" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$PROFILE_NAME" ]; then echo "⚠️ Warning: Failed to extract Name from provisioning profile, will use provided IOS_PROV_PROFILE_NAME" PROFILE_NAME="$IOS_PROV_PROFILE_NAME" fi echo "Profile UUID: $PROFILE_UUID" echo "Profile Name: $PROFILE_NAME" # Install provisioning profile in the correct location with UUID filename echo "Installing provisioning profile in filesystem..." mkdir -p "/Users/runner/Library/MobileDevice/Provisioning Profiles" # Copy with the UUID as filename UUID_TARGET_PATH="/Users/runner/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision" cp -v "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" "$UUID_TARGET_PATH" # Set correct permissions on the profile chmod 644 "$UUID_TARGET_PATH" chown runner:staff "$UUID_TARGET_PATH" # Save the profile path and name to environment for later steps echo "IOS_PROV_PROFILE_PATH=$UUID_TARGET_PATH" >> $GITHUB_ENV echo "IOS_PROV_PROFILE_NAME=$PROFILE_NAME" >> $GITHUB_ENV # Print provisioning profile information echo "Provisioning Profile Information:" echo "--------------------------------" echo "Profile Name (from file): $PROFILE_NAME" echo "Profile Name (from env): $IOS_PROV_PROFILE_NAME" echo "Profile Path: $UUID_TARGET_PATH" echo "Profile UUID: $PROFILE_UUID" echo "App Identifier: $IOS_APP_IDENTIFIER" echo "Team ID: $IOS_TEAM_ID" echo "--------------------------------" # List all provisioning profiles in the system with detailed info echo "List of all provisioning profiles in system:" ls -la "/Users/runner/Library/MobileDevice/Provisioning Profiles/" # Clean up temp file rm -f "$TEMP_PLIST_PATH" echo "✅ Provisioning profile installation steps completed." - name: Build Dependencies (iOS) if: inputs.platform != 'android' run: | echo "🏗️ Building SDK dependencies..." cd ${{ env.APP_PATH }} yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; } echo "✅ Dependencies built successfully" # act won't work with macos, but you can test with `bundle exec fastlane ios ...` - 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 }} GRAFANA_LOKI_USERNAME: ${{ secrets.GRAFANA_LOKI_USERNAME }} IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }} IOS_CONNECT_API_KEY_BASE64: ${{ secrets.IOS_CONNECT_API_KEY_BASE64 }} IOS_CONNECT_API_KEY_PATH: ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }} IOS_CONNECT_ISSUER_ID: ${{ secrets.IOS_CONNECT_ISSUER_ID }} IOS_CONNECT_KEY_ID: ${{ secrets.IOS_CONNECT_KEY_ID }} IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }} IOS_PROJECT_NAME: ${{ secrets.IOS_PROJECT_NAME }} IOS_PROJECT_SCHEME: ${{ secrets.IOS_PROJECT_SCHEME }} IOS_PROV_PROFILE_DIR: ${{ env.IOS_PROV_PROFILE_DIRECTORY }} IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }} IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }} IOS_SIGNING_CERTIFICATE: ${{ secrets.IOS_SIGNING_CERTIFICATE }} IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }} IOS_TEAM_NAME: ${{ secrets.IOS_TEAM_NAME }} IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }} NODE_OPTIONS: "--max-old-space-size=8192" SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} timeout-minutes: 90 run: | cd ${{ env.APP_PATH }} echo "--- Pre-Fastlane Diagnostics ---" echo "Running as user: $(whoami)" echo "Default keychain:" security list-keychains -d user echo "Identities in build.keychain:" security find-identity -v -p codesigning build.keychain || echo "Failed to find identities in build.keychain" echo "--- Starting Fastlane ---" # Determine deployment track and version bump DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}" 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 (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:skip \ test_mode:true \ --verbose else echo "🚀 Deploying to App Store Connect..." bundle exec fastlane ios deploy_auto \ deployment_track:$DEPLOYMENT_TRACK \ 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 if: inputs.platform != 'android' run: | PBXPROJ_FILE="app/ios/Self.xcodeproj/project.pbxproj" # Create a temporary file to store version info echo "Extracting version information..." rm -f versions.txt grep -E 'CURRENT_PROJECT_VERSION = [0-9]+;|MARKETING_VERSION = [0-9]+\.[0-9]+\.[0-9]+;' "${PBXPROJ_FILE}" > versions.txt # Check if we have version information if [ -s versions.txt ]; then echo "Found version information. Resetting file and re-applying versions..." # Store the version values CURRENT_VERSION=$(grep 'CURRENT_PROJECT_VERSION' versions.txt | head -1 | sed 's/.*CURRENT_PROJECT_VERSION = \([0-9]*\);.*/\1/') MARKETING_VERSION=$(grep 'MARKETING_VERSION' versions.txt | head -1 | sed 's/.*MARKETING_VERSION = \([0-9]*\.[0-9]*\.[0-9]*\);.*/\1/') echo "Current version: $CURRENT_VERSION" echo "Marketing version: $MARKETING_VERSION" # Reset the file to HEAD git checkout HEAD -- "${PBXPROJ_FILE}" # Update the versions if they exist if [ ! -z "$CURRENT_VERSION" ]; then sed -i '' "s/\(CURRENT_PROJECT_VERSION = \)[0-9]*;/\1$CURRENT_VERSION;/g" "${PBXPROJ_FILE}" fi if [ ! -z "$MARKETING_VERSION" ]; then sed -i '' "s/\(MARKETING_VERSION = \)[0-9]*\.[0-9]*\.[0-9]*;/\1$MARKETING_VERSION;/g" "${PBXPROJ_FILE}" fi echo "Version information successfully applied." else echo "No version information found. Resetting file..." git checkout HEAD -- "${PBXPROJ_FILE}" fi # Clean up rm -f versions.txt - name: Monitor cache usage if: always() run: | echo "📊 Cache Size Report (iOS Build)" echo "================================" if [ -d "${{ env.APP_PATH }}/node_modules" ]; then NODE_SIZE=$(du -sh "${{ env.APP_PATH }}/node_modules" | cut -f1) echo "Node modules: $NODE_SIZE" fi 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 if [ -d "${{ env.APP_PATH }}/ios/Pods" ]; then PODS_SIZE=$(du -sh "${{ env.APP_PATH }}/ios/Pods" | cut -f1) echo "CocoaPods: $PODS_SIZE" fi echo "================================" echo "💡 GitHub Actions cache limit: 10GB per repository" build-android: needs: [bump-version] runs-on: ubuntu-latest permissions: contents: read actions: write id-token: write if: | (github.event_name != 'pull_request' || github.event.pull_request.merged == true) && ( (inputs.platform == 'android' || inputs.platform == 'both') || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'deploy:ios')) ) steps: - name: Mobile deployment status run: | echo "🚀 Mobile deployment is enabled - proceeding with Android build" echo "📱 Platform: ${{ inputs.platform || 'both' }}" echo "🎯 Track: ${{ inputs.deployment_track || 'internal' }}" echo "📦 Version bump: ${{ inputs.version_bump || 'build' }}" if [ "${{ github.event_name }}" = "pull_request" ]; then echo "🔀 Triggered by PR merge: #${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}" echo "👤 Merged by: ${{ github.event.pull_request.merged_by.login }}" if ${{ contains(github.event.pull_request.labels.*.name, 'deploy:android') }}; then echo "🏷️ Label: deploy:android (skipping iOS)" fi fi - uses: actions/checkout@v4 if: inputs.platform != 'ios' with: fetch-depth: 0 # Checkout the branch that triggered the workflow ref: ${{ github.ref_name }} - uses: "google-github-actions/auth@v2" with: project_id: "plucky-tempo-454713-r0" workload_identity_provider: "projects/852920390127/locations/global/workloadIdentityPools/gh-self/providers/github-by-repos" service_account: "self-xyz@plucky-tempo-454713-r0.iam.gserviceaccount.com" - name: Free up disk space uses: ./.github/actions/free-disk-space # Fail fast: set up JDK for keytool and verify Android secrets early - name: Setup Java environment if: inputs.platform != 'ios' uses: actions/setup-java@v4 with: distribution: "temurin" java-version: ${{ env.JAVA_VERSION }} - name: Decode Android Secrets if: inputs.platform != 'ios' run: | echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }} - name: Verify Android Secrets if: inputs.platform != 'ios' run: | # Verify Google Cloud auth via Workload Identity Federation (ADC) if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ] || [ ! -f "$GOOGLE_APPLICATION_CREDENTIALS" ]; then echo "❌ Error: GOOGLE_APPLICATION_CREDENTIALS not set or file missing. Ensure google-github-actions/auth ran." exit 1 fi # Verify keystore file exists and is valid if [ ! -f "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" ]; then echo "❌ Error: Keystore file was not created successfully" exit 1 fi # Try to verify the keystore with the provided password if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >/dev/null 2>&1; then echo "❌ Error: Invalid keystore password" exit 1 fi # Verify the key alias exists if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" >/dev/null 2>&1; then echo "❌ Error: Key alias '${{ secrets.ANDROID_KEY_ALIAS }}' not found in keystore" exit 1 fi # Verify the key password if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" >/dev/null 2>&1; then echo "❌ Error: Invalid key password" exit 1 fi # Detect keystore type and export for later steps KEYSTORE_TYPE=$(keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" 2>/dev/null | awk -F': ' '/Keystore type:/ {print $2; exit}') if [ -z "$KEYSTORE_TYPE" ]; then echo "❌ Error: Unable to determine keystore type" exit 1 fi echo "ANDROID_KEYSTORE_TYPE=$KEYSTORE_TYPE" >> "$GITHUB_ENV" echo "Detected keystore type: $KEYSTORE_TYPE" # Ensure the alias holds a PrivateKeyEntry (required for signing) if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" | grep -q "Entry type: PrivateKeyEntry"; then echo "❌ Error: Alias '${{ secrets.ANDROID_KEY_ALIAS }}' is not a PrivateKeyEntry" exit 1 fi echo "✅ All Android secrets verified successfully!" - name: Read and sanitize Node.js version shell: bash run: | if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then echo "❌ .nvmrc is missing or empty"; exit 1; fi VERSION="$(tr -d '\r\n' < .nvmrc)" VERSION="${VERSION#v}" if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then echo "Invalid .nvmrc content: '$VERSION'"; exit 1; fi echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" - name: Verify branch and commit (Android) if: needs.bump-version.outputs.platform != 'ios' run: | echo "🔍 Verifying we're building from the correct branch and commit..." 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')" 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 $BUILD_BRANCH commit" fi - name: Apply version bump for build if: needs.bump-version.outputs.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 with: path: | .yarn/cache .yarn/install-state.gz .yarn/unplugged cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }} - name: Cache Ruby gems id: gems-cache uses: ./.github/actions/cache-bundler with: 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 }} - name: Cache Gradle dependencies id: gradle-cache uses: ./.github/actions/cache-gradle with: cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GRADLE_CACHE_VERSION }} - name: Cache Android NDK id: ndk-cache uses: actions/cache@v4 with: path: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.ANDROID_NDK_VERSION }} key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }} - name: Log cache status run: | echo "Cache hit results:" echo "- Yarn cache hit: ${{ steps.yarn-cache.outputs.cache-hit }}" echo "- Gems cache hit: ${{ steps.gems-cache.outputs.cache-hit }}" echo "- Gradle cache hit: ${{ steps.gradle-cache.outputs.cache-hit }}" echo "- NDK cache hit: ${{ steps.ndk-cache.outputs.cache-hit }}" - name: Disable Yarn hardened mode run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV - name: Verify lock files are up to date run: | echo "🔍 Checking if lock files are in sync with dependency files..." # For yarn workspaces, yarn.lock is at root if [ ! -f "${{ env.WORKSPACE }}/yarn.lock" ]; then echo "❌ ERROR: yarn.lock file is missing at workspace root!" echo "Run 'yarn install' at the repository root and commit the yarn.lock file." exit 1 fi # Gemfile.lock is in app directory if [ ! -f "${{ env.APP_PATH }}/Gemfile.lock" ]; then echo "❌ ERROR: Gemfile.lock file is missing!" echo "Run 'bundle install' in the app directory and commit the Gemfile.lock file." exit 1 fi echo "✅ Lock files exist" - 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 }} node_version: ${{ env.NODE_VERSION }} ruby_version: ${{ env.RUBY_VERSION }} workspace: ${{ env.WORKSPACE }} env: 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 if: inputs.platform != 'ios' uses: android-actions/setup-android@v3 with: accept-android-sdk-licenses: true - name: Install NDK if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true' uses: nick-fields/retry@v3 with: timeout_minutes: 15 max_attempts: 5 retry_wait_seconds: 10 command: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}" - name: Install CMake if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true' uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 5 retry_wait_seconds: 10 command: sdkmanager "cmake;3.22.1" - name: Set Gradle JVM options if: inputs.platform != 'ios' # Apply to CI builds (not just ACT) run: | echo "org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8" >> ${{ env.APP_PATH }}/android/gradle.properties - name: Install Python dependencies for Play Store upload if: inputs.platform != 'ios' run: | python -m pip install --upgrade pip pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client - name: Clone android-passport-nfc-reader if: inputs.platform != 'ios' uses: ./.github/actions/clone-android-passport-nfc-reader with: working_directory: ${{ env.APP_PATH }} selfxyz_internal_pat: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} - name: Build Dependencies (Android) if: inputs.platform != 'ios' run: | echo "🏗️ Building SDK dependencies..." cd ${{ env.APP_PATH }} yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; } echo "✅ Dependencies built successfully" - 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 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }} ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }} ENABLE_DEBUG_LOGS: ${{ secrets.ENABLE_DEBUG_LOGS }} GOOGLE_SIGNIN_ANDROID_CLIENT_ID: ${{ secrets.GOOGLE_SIGNIN_ANDROID_CLIENT_ID }} GRAFANA_LOKI_PASSWORD: ${{ secrets.GRAFANA_LOKI_PASSWORD }} GRAFANA_LOKI_URL: ${{ secrets.GRAFANA_LOKI_URL }} GRAFANA_LOKI_USERNAME: ${{ secrets.GRAFANA_LOKI_USERNAME }} NODE_OPTIONS: "--max-old-space-size=6144" SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} run: | cd ${{ env.APP_PATH }} # Determine deployment track and version bump DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}" 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 (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: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: Clean up Gradle build artifacts if: inputs.platform != 'ios' uses: ./.github/actions/cleanup-gradle-artifacts - name: Upload to Google Play Store using WIF if: inputs.platform != 'ios' && inputs.test_mode != true timeout-minutes: 10 run: | cd ${{ env.APP_PATH }} # Determine deployment track DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}" echo "🚀 Uploading to Google Play Store using Workload Identity Federation..." python scripts/upload_to_play_store.py \ --aab "android/app/build/outputs/bundle/release/app-release.aab" \ --package-name "${{ secrets.ANDROID_PACKAGE_NAME }}" \ --track "$DEPLOYMENT_TRACK" - name: Monitor cache usage if: always() run: | echo "📊 Cache Size Report (Android Build)" echo "====================================" if [ -d "${{ env.APP_PATH }}/node_modules" ]; then NODE_SIZE=$(du -sh "${{ env.APP_PATH }}/node_modules" | cut -f1) 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) echo "Ruby gems: $GEMS_SIZE" fi if [ -d "$HOME/.gradle/caches" ]; then GRADLE_SIZE=$(du -sh "$HOME/.gradle/caches" | cut -f1) echo "Gradle caches: $GRADLE_SIZE" fi if [ -d "${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.ANDROID_NDK_VERSION }}" ]; then NDK_SIZE=$(du -sh "${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.ANDROID_NDK_VERSION }}" | cut -f1) echo "Android NDK: $NDK_SIZE" fi echo "====================================" 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 permissions: contents: write pull-requests: write needs: [bump-version, build-ios, build-android] if: | always() && (github.event_name != 'pull_request' || github.event.pull_request.merged == true) && (needs.build-ios.result == 'success' || needs.build-android.result == 'success') env: APP_PATH: ${{ github.workspace }}/app steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Checkout target branch for version bump PR (default: dev, override with bump_target_branch input) ref: ${{ inputs.bump_target_branch || 'dev' }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: .nvmrc - 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 }}" IOS_SUCCESS="${{ needs.build-ios.result }}" ANDROID_SUCCESS="${{ needs.build-android.result }}" 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 and deployment timestamps node -e " const fs = require('fs'); const version = JSON.parse(fs.readFileSync('version.json', 'utf8')); const timestamp = new Date().toISOString(); version.ios.build = $IOS_BUILD; version.android.build = $ANDROID_BUILD; // Update lastDeployed timestamp for successful builds if ('$IOS_SUCCESS' === 'success') { version.ios.lastDeployed = timestamp; console.log('✅ Updated iOS lastDeployed timestamp'); } if ('$ANDROID_SUCCESS' === 'success') { version.android.lastDeployed = timestamp; console.log('✅ Updated Android lastDeployed timestamp'); } fs.writeFileSync('version.json', JSON.stringify(version, null, 2) + '\n'); console.log('✅ Updated version.json'); " echo "✅ Versions applied successfully" - name: Verify version changes run: | cd ${{ env.APP_PATH }} # Check that version files actually changed if ! git diff --quiet package.json version.json; then echo "✅ Version changes detected" git diff package.json version.json else echo "⚠️ No version changes detected in package.json or version.json" echo "This may indicate a problem with version application" exit 1 fi - name: Determine platforms that succeeded and PR title id: platforms run: | 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 [ "$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' }}" # Add timestamp to branch name to avoid collisions TIMESTAMP=$(date +%s%N | cut -b1-13) # Milliseconds since epoch (13 digits) BRANCH_NAME="ci/bump-mobile-version-${VERSION}-${TIMESTAMP}" 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 a PR already exists for this version (avoid duplicate PRs) EXISTING_PR=$(gh pr list --base "${TARGET_BRANCH}" --state open --json number,title,headRefName --jq ".[] | select(.title | contains(\"${VERSION}\")) | .number" | head -1) if [ -n "$EXISTING_PR" ]; then echo "⚠️ PR #${EXISTING_PR} already exists for version ${VERSION}" echo "ℹ️ Skipping PR creation to avoid duplicates" echo "ℹ️ Existing PR: https://github.com/${{ github.repository }}/pull/${EXISTING_PR}" 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" -m "Update build numbers and deployment timestamps after successful deployment." # 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 updates: - Build numbers for deployed platforms - Deployment timestamps (\`lastDeployed\`) for successful builds 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: runs-on: ubuntu-latest permissions: contents: write 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') steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Checkout target branch for tagging (usually dev) ref: ${{ inputs.bump_target_branch || 'dev' }} token: ${{ secrets.GITHUB_TOKEN }} - name: Configure Git run: | git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" - name: Create and push tags run: | cd ${{ env.APP_PATH }} # 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 (idempotent) if git tag -a "v${VERSION}" -m "Release ${VERSION}" 2>/dev/null; then echo "✅ Created tag: v${VERSION}" else 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 (idempotent) if [ "${{ needs.build-ios.result }}" = "success" ]; then TAG_NAME="v${VERSION}-ios-${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 -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 (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 "") # Generate simple changelog echo "## What's Changed" > release_notes.md echo "" >> release_notes.md if [ -n "$PREV_TAG" ]; then git log --pretty=format:"- %s" ${PREV_TAG}..HEAD --no-merges | grep -v "^- Merge" >> release_notes.md else echo "Initial release" >> release_notes.md fi echo "" >> release_notes.md echo "## Build Information" >> release_notes.md echo "- iOS Build: ${IOS_BUILD}" >> release_notes.md echo "- Android Build: ${ANDROID_BUILD}" >> release_notes.md # Set output for next step echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.changelog.outputs.version }} name: Release ${{ steps.changelog.outputs.version }} body_path: ${{ env.APP_PATH }}/release_notes.md draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}