Files
self/.github/workflows/mobile-deploy.yml
Justin Hernandez 64ab5fc91c Minor app fixes two point nine rd2 (#1462)
* better tests

* lockfile naming fixes

* format

* fix ci issues
2025-12-02 22:20:17 -08:00

1572 lines
66 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
# 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
# Guard against auto-closed PRs (GitHub reports merged == false when a PR closes without merging)
if: |
(
github.event_name != 'pull_request' ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)
) &&
(
github.event_name != 'pull_request' ||
!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.action == 'closed' && 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: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- 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 }}-${{ steps.yarnrc-hash.outputs.hash }}
- 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
lockfile: 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 }}
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }}
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.action == 'closed' && 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: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- 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 }}-${{ steps.yarnrc-hash.outputs.hash }}
- 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: Setup Android private modules
if: inputs.platform != 'ios'
run: |
cd ${{ env.APP_PATH }}
PLATFORM=android node scripts/setup-private-modules.cjs
env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
CI: true
- 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 }}
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }}
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.action == 'closed' && 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();
// Only bump build numbers for successful builds
if ('$IOS_SUCCESS' === 'success') {
version.ios.build = $IOS_BUILD;
version.ios.lastDeployed = timestamp;
console.log('✅ Updated iOS build number to $IOS_BUILD and lastDeployed timestamp');
} else {
console.log('⏭️ Skipped iOS build number bump (build did not succeed)');
}
if ('$ANDROID_SUCCESS' === 'success') {
version.android.build = $ANDROID_BUILD;
version.android.lastDeployed = timestamp;
console.log('✅ Updated Android build number to $ANDROID_BUILD and lastDeployed timestamp');
} else {
console.log('⏭️ Skipped Android build number bump (build did not succeed)');
}
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 }}