Files
self/.github/workflows/mobile-deploy.yml
Justin Hernandez ff678b359a chore: fix yarn format (#1009)
* fix yarn format

* yarn format

* fix lint

* undo temporary disabling

* pipeline fixes

* revert nvmrc change
2025-09-07 19:48:46 -07:00

1123 lines
48 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }}