mirror of
https://github.com/selfxyz/self.git
synced 2026-01-08 06:14:07 -05:00
* fix yarn format * yarn format * fix lint * undo temporary disabling * pipeline fixes * revert nvmrc change
1123 lines
48 KiB
YAML
1123 lines
48 KiB
YAML
name: Mobile Deploy
|
||
|
||
env:
|
||
# Build environment versions
|
||
RUBY_VERSION: 3.2
|
||
JAVA_VERSION: 17
|
||
ANDROID_API_LEVEL: 35
|
||
ANDROID_NDK_VERSION: 27.0.11718014
|
||
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: write
|
||
pull-requests: write
|
||
id-token: write
|
||
|
||
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
|
||
|
||
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: mobile-deploy-${{ inputs.deployment_track || github.ref_name }}
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
build-ios:
|
||
runs-on: macos-latest-large
|
||
if: (inputs.platform == 'ios' || inputs.platform == 'both')
|
||
steps:
|
||
- name: Mobile deployment status
|
||
run: |
|
||
echo "🚀 Mobile deployment is enabled - proceeding with iOS build"
|
||
echo "📱 Platform: ${{ inputs.platform }}"
|
||
echo "🎯 Track: ${{ inputs.deployment_track }}"
|
||
echo "📦 Version bump: ${{ inputs.version_bump }}"
|
||
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
ref: staging
|
||
- 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: inputs.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')"
|
||
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
|
||
else
|
||
echo "✅ Building from latest staging commit"
|
||
fi
|
||
|
||
- 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 dependencies
|
||
id: yarn-cache
|
||
uses: ./.github/actions/cache-yarn
|
||
with:
|
||
path: |
|
||
.yarn/cache
|
||
node_modules
|
||
${{ env.APP_PATH }}/node_modules
|
||
key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-${{ github.sha }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-
|
||
|
||
- name: Cache Ruby gems
|
||
id: gems-cache
|
||
uses: ./.github/actions/cache-bundler
|
||
with:
|
||
path: ${{ env.APP_PATH }}/ios/vendor/bundle
|
||
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}-
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_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 }}-${{ github.sha }}
|
||
|
||
- 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: 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
|
||
if: inputs.platform != 'android'
|
||
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:
|
||
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_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
|
||
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
|
||
IOS_TEAM_NAME: ${{ secrets.IOS_TEAM_NAME }}
|
||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
|
||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||
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="${{ inputs.version_bump || 'build' }}"
|
||
TEST_MODE="${{ inputs.test_mode || false }}"
|
||
|
||
echo "📱 Deployment Configuration:"
|
||
echo " - Track: $DEPLOYMENT_TRACK"
|
||
echo " - Version Bump: $VERSION_BUMP"
|
||
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 \
|
||
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 \
|
||
--verbose
|
||
fi
|
||
|
||
# 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: Get version from package.json
|
||
if: inputs.platform != 'android'
|
||
uses: ./.github/actions/get-version
|
||
with:
|
||
app_path: ${{ env.APP_PATH }}
|
||
|
||
- name: Open PR for iOS build number bump
|
||
if: ${{ !env.ACT && success() }}
|
||
uses: peter-evans/create-pull-request@v6
|
||
with:
|
||
title: "chore: bump iOS build for ${{ env.VERSION }}"
|
||
body: "Automated bump of iOS build number by CI"
|
||
commit-message: "chore: incrementing ios build number for version ${{ env.VERSION }} [github action]"
|
||
branch: ci/bump-ios-build-${{ github.run_id }}
|
||
base: staging
|
||
add-paths: |
|
||
app/version.json
|
||
|
||
- 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 }}/ios/vendor/bundle" ]; then
|
||
GEMS_SIZE=$(du -sh "${{ env.APP_PATH }}/ios/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:
|
||
runs-on: ubuntu-latest
|
||
if: (inputs.platform == 'android' || inputs.platform == 'both')
|
||
steps:
|
||
- name: Mobile deployment status
|
||
run: |
|
||
echo "🚀 Mobile deployment is enabled - proceeding with Android build"
|
||
echo "📱 Platform: ${{ inputs.platform }}"
|
||
echo "🎯 Track: ${{ inputs.deployment_track }}"
|
||
echo "📦 Version bump: ${{ inputs.version_bump }}"
|
||
|
||
- uses: actions/checkout@v4
|
||
if: inputs.platform != 'ios'
|
||
with:
|
||
fetch-depth: 0
|
||
ref: staging
|
||
- 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"
|
||
# 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: inputs.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')"
|
||
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
|
||
else
|
||
echo "✅ Building from latest staging commit"
|
||
fi
|
||
|
||
- name: Cache Yarn dependencies
|
||
id: yarn-cache
|
||
uses: ./.github/actions/cache-yarn
|
||
with:
|
||
path: |
|
||
.yarn/cache
|
||
node_modules
|
||
${{ env.APP_PATH }}/node_modules
|
||
key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-${{ github.sha }}
|
||
restore-keys: |
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-${{ hashFiles('yarn.lock') }}-
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-${{ env.GH_YARN_CACHE_VERSION }}-
|
||
${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ env.GH_CACHE_VERSION }}-
|
||
|
||
- name: Cache Ruby gems
|
||
id: gems-cache
|
||
uses: ./.github/actions/cache-bundler
|
||
with:
|
||
path: ${{ env.APP_PATH }}/ios/vendor/bundle
|
||
key: ${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-${{ hashFiles('app/Gemfile.lock') }}-
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-
|
||
${{ runner.os }}-ruby${{ env.RUBY_VERSION }}-gems-${{ env.GH_CACHE_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 }}-${{ github.sha }}
|
||
|
||
- 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 }}-${{ github.sha }}
|
||
|
||
- 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: 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
|
||
if: inputs.platform != 'ios'
|
||
uses: ./.github/actions/mobile-setup
|
||
with:
|
||
app_path: ${{ env.APP_PATH }}
|
||
node_version: ${{ env.NODE_VERSION }}
|
||
ruby_version: ${{ env.RUBY_VERSION }}
|
||
workspace: ${{ env.WORKSPACE }}
|
||
|
||
# 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 and CMake
|
||
if: inputs.platform != 'ios' && steps.ndk-cache.outputs.cache-hit != 'true'
|
||
run: |
|
||
max_attempts=5
|
||
attempt=1
|
||
|
||
# Install NDK
|
||
while [ $attempt -le $max_attempts ]; do
|
||
echo "Attempt $attempt of $max_attempts to install NDK..."
|
||
if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then
|
||
echo "Successfully installed NDK"
|
||
break
|
||
fi
|
||
echo "Failed to install NDK on attempt $attempt"
|
||
if [ $attempt -eq $max_attempts ]; then
|
||
echo "All attempts to install NDK failed"
|
||
exit 1
|
||
fi
|
||
# Exponential backoff: 2^attempt seconds
|
||
wait_time=$((2 ** attempt))
|
||
echo "Waiting $wait_time seconds before retrying..."
|
||
sleep $wait_time
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
# Install CMake (required for native module builds)
|
||
echo "Installing CMake..."
|
||
attempt=1
|
||
while [ $attempt -le $max_attempts ]; do
|
||
echo "Attempt $attempt of $max_attempts to install CMake..."
|
||
if sdkmanager "cmake;3.22.1"; then
|
||
echo "Successfully installed CMake"
|
||
break
|
||
fi
|
||
echo "Failed to install CMake on attempt $attempt"
|
||
if [ $attempt -eq $max_attempts ]; then
|
||
echo "All attempts to install CMake failed"
|
||
exit 1
|
||
fi
|
||
# Exponential backoff: 2^attempt seconds
|
||
wait_time=$((2 ** attempt))
|
||
echo "Waiting $wait_time seconds before retrying..."
|
||
sleep $wait_time
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
- 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: 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:
|
||
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEYSTORE_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }}
|
||
NODE_OPTIONS: "--max-old-space-size=6144"
|
||
run: |
|
||
cd ${{ env.APP_PATH }}
|
||
|
||
# Determine deployment track and version bump
|
||
DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}"
|
||
VERSION_BUMP="${{ inputs.version_bump || 'build' }}"
|
||
TEST_MODE="${{ inputs.test_mode || false }}"
|
||
|
||
echo "🤖 Build Configuration:"
|
||
echo " - Track: $DEPLOYMENT_TRACK"
|
||
echo " - Version Bump: $VERSION_BUMP"
|
||
echo " - Test Mode: $TEST_MODE"
|
||
|
||
echo "🔨 Building AAB with Fastlane..."
|
||
bundle exec fastlane android build_only \
|
||
deployment_track:$DEPLOYMENT_TRACK \
|
||
version_bump:$VERSION_BUMP \
|
||
--verbose
|
||
|
||
- name: Upload to Google Play Store using WIF
|
||
if: inputs.platform != 'ios' && inputs.test_mode != true
|
||
env:
|
||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
|
||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||
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"
|
||
|
||
# Version updates moved to separate job to avoid race conditions
|
||
|
||
- name: Get version from package.json
|
||
if: inputs.platform != 'ios'
|
||
uses: ./.github/actions/get-version
|
||
with:
|
||
app_path: ${{ env.APP_PATH }}
|
||
|
||
- name: Open PR for Android build number bump
|
||
if: ${{ !env.ACT && success() }}
|
||
uses: peter-evans/create-pull-request@v6
|
||
with:
|
||
title: "chore: bump Android build for ${{ env.VERSION }}"
|
||
body: "Automated bump of Android build number by CI"
|
||
commit-message: "chore: incrementing android build version for version ${{ env.VERSION }} [github action]"
|
||
branch: ci/bump-android-build-${{ github.run_id }}
|
||
base: staging
|
||
add-paths: |
|
||
app/version.json
|
||
|
||
- 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"
|
||
|
||
# Separate job to update version files after successful deployment
|
||
# This avoids race conditions when both iOS and Android run in parallel
|
||
update-version:
|
||
runs-on: ubuntu-latest
|
||
needs: [build-ios, build-android]
|
||
if: |
|
||
always() &&
|
||
inputs.test_mode != true &&
|
||
(needs.build-ios.result == 'success' || needs.build-android.result == 'success')
|
||
env:
|
||
APP_PATH: ${{ github.workspace }}/app
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
token: ${{ github.token }}
|
||
fetch-depth: 0
|
||
ref: staging
|
||
- 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"
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: ${{ env.NODE_VERSION }}
|
||
|
||
- name: Update package.json version
|
||
run: |
|
||
cd ${{ env.APP_PATH }}
|
||
|
||
# Get current version from package.json
|
||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||
|
||
# Get new version from version.json (if it exists and has version field)
|
||
if [ -f version.json ] && grep -q '"version"' version.json; then
|
||
NEW_VERSION=$(node -pe 'require("./version.json").version' 2>/dev/null || echo "")
|
||
else
|
||
# Fallback: use current version from package.json
|
||
NEW_VERSION="$CURRENT_VERSION"
|
||
fi
|
||
|
||
# Only update if versions differ
|
||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ] && [ -n "$NEW_VERSION" ]; then
|
||
echo "📦 Updating package.json version:"
|
||
echo " From: v$CURRENT_VERSION"
|
||
echo " To: v$NEW_VERSION"
|
||
|
||
# Use yarn to update package.json and the lockfile
|
||
yarn version --new-version "$NEW_VERSION" --no-git-tag-version -y
|
||
else
|
||
echo "ℹ️ Version already up to date or no version field in version.json"
|
||
fi
|
||
|
||
- name: Open PR to update version files
|
||
uses: peter-evans/create-pull-request@v6
|
||
with:
|
||
title: "chore: update version files after deployment"
|
||
body: |
|
||
Automated update of version files after successful deployment.
|
||
Includes updates to `app/version.json`, `app/package.json`, and `yarn.lock`.
|
||
commit-message: "chore: update version files after deployment [skip ci]"
|
||
branch: ci/update-version-${{ github.run_id }}
|
||
base: staging
|
||
add-paths: |
|
||
app/version.json
|
||
app/package.json
|
||
yarn.lock
|
||
|
||
# Create git tags after successful deployment
|
||
create-release-tags:
|
||
needs: [build-ios, build-android, update-version]
|
||
if: |
|
||
always() &&
|
||
needs.update-version.result == 'success' &&
|
||
(needs.build-ios.result == 'success' || needs.build-android.result == 'success') &&
|
||
inputs.deployment_track == 'production'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
ref: staging
|
||
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 }}
|
||
|
||
# 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)
|
||
|
||
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}"
|
||
echo "✅ Created tag: v${VERSION}"
|
||
else
|
||
echo "⏭️ Tag v${VERSION} already exists"
|
||
fi
|
||
|
||
# Create platform-specific tags if deployments succeeded
|
||
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})"
|
||
echo "✅ Created tag: ${TAG_NAME}"
|
||
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})"
|
||
echo "✅ Created tag: ${TAG_NAME}"
|
||
fi
|
||
fi
|
||
|
||
# Push all tags
|
||
git push origin --tags
|
||
echo "🚀 Tags pushed to repository"
|
||
|
||
- name: Generate changelog for release
|
||
id: changelog
|
||
run: |
|
||
cd ${{ env.APP_PATH }}
|
||
|
||
# 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 }}
|