Skip to content

Merge pull request #20 from openSVM/copilot/fix-19 #156

Merge pull request #20 from openSVM/copilot/fix-19

Merge pull request #20 from openSVM/copilot/fix-19 #156

Workflow file for this run

name: Build Android APK
on:
push:
branches: [ main ]
tags: [ 'v*' ]
paths-ignore:
- 'docs/**'
- '*.md'
release:
types: [ published ]
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: Verify craco installation
run: |
echo "Checking craco installation..."
yarn list @craco/craco
echo "Checking if craco command is available..."
npx craco --version || echo "Craco not found via npx"
- 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: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
api-level: 35
build-tools: 35.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