mirror of
https://github.com/selfxyz/self.git
synced 2026-01-07 22:04:03 -05:00
1572 lines
66 KiB
YAML
1572 lines
66 KiB
YAML
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 }}
|