Skip to content

Comprehensive React 19 upgrade with MUI v7 compatibility, dynamic theming, production deployment fixes, and Android build compatibility #125

Comprehensive React 19 upgrade with MUI v7 compatibility, dynamic theming, production deployment fixes, and Android build compatibility

Comprehensive React 19 upgrade with MUI v7 compatibility, dynamic theming, production deployment fixes, and Android build compatibility #125

Workflow file for this run

name: Build Android APK
on:
push:
branches: [ main, develop, 'feature/*', 'fix/*', 'copilot/*' ]
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
branches: [ main, develop ]
paths-ignore:
- 'docs/**'
- '*.md'
workflow_dispatch:
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'yarn'
- name: Cache Node modules
uses: actions/cache@v4
with:
path: |
node_modules
~/.yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: |
for i in 1 2 3; do
yarn install --frozen-lockfile && break || sleep 10
done
- name: Build web application
run: yarn build
- name: Initialize Capacitor (if not exists)
run: |
# Check if Capacitor is already configured by looking for config file
CAPACITOR_CONFIG=""
if [ -f "capacitor.config.json" ]; then
CAPACITOR_CONFIG="capacitor.config.json"
elif [ -f "capacitor.config.ts" ]; then
CAPACITOR_CONFIG="capacitor.config.ts"
fi
# Initialize only if no config exists AND no android directory
if [ -z "$CAPACITOR_CONFIG" ] && [ ! -d "android" ]; then
echo "No Capacitor configuration found, initializing..."
npx cap init "SVMSeek Wallet" "com.svmseek.wallet" --web-dir=build
else
echo "Capacitor already configured with: ${CAPACITOR_CONFIG:-android directory}"
fi
- name: Add Android platform (if not exists)
run: |
if [ ! -d "android" ]; then
npx cap add android
fi
- name: Setup Java JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
api-level: 34
build-tools: 34.0.0
- name: Cache Gradle packages
uses: actions/cache@v4
if: hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') != ''
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
android/app/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Sync Capacitor
run: npx cap sync android
- name: Grant execute permission for gradlew
run: chmod +x android/gradlew
- name: Clean Android project
working-directory: ./android
run: |
# Clean project to remove any corrupted resources or build artifacts
./gradlew clean --stacktrace
- name: Build Android APK
working-directory: ./android
run: |
# Configure Gradle for faster builds
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4096m -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=true -Dorg.gradle.caching=true"
./gradlew assembleDebug --stacktrace --build-cache --parallel --configure-on-demand
- name: Setup Production Keystore (if secrets available)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
working-directory: ./android
run: |
# Use environment variable instead of direct secret interpolation in shell
if [ ! -z "$ANDROID_KEYSTORE_BASE64" ]; then
echo "Setting up production keystore..."
# Decode safely with error handling
if echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > release.keystore 2>/dev/null; then
echo "Production keystore created successfully"
else
echo "ERROR: Failed to decode keystore base64 data"
exit 1
fi
else
echo "No production keystore secrets found, skipping production build"
fi
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
- name: Build and Sign Production APK (if production keystore available)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
working-directory: ./android
run: |
if [ -f "release.keystore" ]; then
echo "Building and signing production APK..."
# Clean before production build to prevent resource corruption
./gradlew clean --stacktrace
# Configure signing in gradle.properties for production build
echo "MYAPP_UPLOAD_STORE_FILE=../release.keystore" >> gradle.properties
echo "MYAPP_UPLOAD_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> gradle.properties
echo "MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> gradle.properties
echo "MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> gradle.properties
# Build signed release APK directly (Gradle handles signing automatically)
export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4096m -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=true -Dorg.gradle.caching=true"
./gradlew assembleRelease --stacktrace --build-cache --parallel --configure-on-demand
# Verify the signed APK exists and is properly signed with production-grade validation
if [ -f "app/build/outputs/apk/release/app-release.apk" ]; then
echo "Production APK built successfully, performing comprehensive signature validation..."
# 1. Check APK structure integrity
APK_FILE="app/build/outputs/apk/release/app-release.apk"
# 2. Verify APK can be read without corruption
if ! unzip -t "$APK_FILE" > /dev/null 2>&1; then
echo "ERROR: APK file is corrupted or invalid"
exit 1
fi
# 3. Check for proper signature files (META-INF directory)
SIGNATURE_FILES=$(unzip -l "$APK_FILE" | grep -E "META-INF.*\.(RSA|DSA|EC)" | wc -l)
if [ "$SIGNATURE_FILES" -eq 0 ]; then
echo "ERROR: No signature files found in APK"
exit 1
fi
echo "Found $SIGNATURE_FILES signature file(s)"
# 4. Verify APK signature using jarsigner (more thorough than grep)
if command -v jarsigner >/dev/null 2>&1; then
echo "Verifying APK signature with jarsigner..."
if jarsigner -verify -verbose "$APK_FILE" > /tmp/apk_verify.log 2>&1; then
echo "✓ APK signature verification passed"
# Check for timestamp and certificate details
if grep -q "jar verified" /tmp/apk_verify.log; then
echo "✓ APK is properly signed and verified"
else
echo "WARNING: APK verification completed but with potential issues"
cat /tmp/apk_verify.log
fi
else
echo "ERROR: APK signature verification failed"
cat /tmp/apk_verify.log
exit 1
fi
else
echo "WARNING: jarsigner not available, using basic signature check"
unzip -l "$APK_FILE" | grep -E "META-INF.*\.(RSA|DSA|EC)" && echo "✓ Basic signature files present" || (echo "ERROR: No signature files found" && exit 1)
fi
# 5. Check APK size is reasonable (not empty or suspiciously small)
echo "Validating APK size..."
if ! ./scripts/check-apk-size.sh "$APK_FILE" 1; then
echo "ERROR: APK size validation failed"
exit 1
fi
# 6. Verify APK contains expected Android components
REQUIRED_FILES="AndroidManifest.xml classes.dex resources.arsc"
for required_file in $REQUIRED_FILES; do
if ! unzip -l "$APK_FILE" | grep -q "$required_file"; then
echo "ERROR: Required Android component '$required_file' not found in APK"
exit 1
fi
done
echo "✓ All required Android components present"
echo "🎉 Production APK passed comprehensive validation"
else
echo "Production APK not found after build"
ls -la app/build/outputs/apk/release/ || echo "Release output directory not found"
exit 1
fi
else
echo "No production keystore found, skipping production build"
fi
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
- name: Verify APKs
working-directory: ./android
run: |
echo "Verifying debug APK..."
if [ -f "app/build/outputs/apk/debug/app-debug.apk" ]; then
# Basic APK structure validation
unzip -l app/build/outputs/apk/debug/app-debug.apk > /dev/null && echo "Debug APK structure is valid"
echo "Debug APK verified successfully"
else
echo "Debug APK not found for verification"
ls -la app/build/outputs/apk/debug/ || echo "Debug output directory not found"
exit 1
fi
# Verify production APK if it exists
if [ -f "app/build/outputs/apk/release/app-release.apk" ]; then
echo "Verifying production APK..."
unzip -l app/build/outputs/apk/release/app-release.apk > /dev/null && echo "Production APK structure is valid"
echo "Production APK verified successfully"
fi
- name: Rename APK with version
run: |
VERSION=$(node -p "require('./package.json').version")
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Copy debug APK (always available)
cp android/app/build/outputs/apk/debug/app-debug.apk svmseek-wallet-debug-v${VERSION}-${TIMESTAMP}.apk
# Copy production APK if available
if [ -f "android/app/build/outputs/apk/release/app-release.apk" ]; then
cp android/app/build/outputs/apk/release/app-release.apk svmseek-wallet-production-v${VERSION}-${TIMESTAMP}.apk
echo "Production APK renamed: svmseek-wallet-production-v${VERSION}-${TIMESTAMP}.apk"
fi
echo "Debug APK renamed: svmseek-wallet-debug-v${VERSION}-${TIMESTAMP}.apk"
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: svmseek-wallet-apk
path: svmseek-wallet-*.apk
retention-days: 30
- name: Upload Production APK as release asset (on main branch)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: svmseek-wallet-production-apk
path: svmseek-wallet-production-*.apk
retention-days: 90
if-no-files-found: ignore