Fix Capacitor init step logic and convert config to JSON format #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: yarn install --frozen-lockfile | |
| - name: Build web application | |
| run: yarn build | |
| - name: Initialize Capacitor (if not exists) | |
| run: | | |
| if [ ! -d "android" ] && [ ! -f "capacitor.config.json" ] && [ ! -f "capacitor.config.ts" ]; then | |
| npx cap init "SVMSeek Wallet" "com.svmseek.wallet" --web-dir=build | |
| 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: | | |
| if [ ! -z "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ]; then | |
| echo "Setting up production keystore..." | |
| echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore | |
| echo "Production keystore created" | |
| 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) | |
| APK_SIZE=$(stat -f%z "$APK_FILE" 2>/dev/null || stat -c%s "$APK_FILE" 2>/dev/null || echo "0") | |
| MIN_SIZE=$((1024 * 1024)) # 1MB minimum | |
| if [ "$APK_SIZE" -lt "$MIN_SIZE" ]; then | |
| echo "ERROR: APK size ($APK_SIZE bytes) is suspiciously small" | |
| exit 1 | |
| fi | |
| echo "✓ APK size validation passed: $(( APK_SIZE / 1024 / 1024 ))MB" | |
| # 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 |