1+ name : Build Android APK
2+
3+ on :
4+ push :
5+ branches : [ main, develop, 'feature/*', 'fix/*', 'copilot/*' ]
6+ paths-ignore :
7+ - ' docs/**'
8+ - ' *.md'
9+ pull_request :
10+ branches : [ main, develop ]
11+ paths-ignore :
12+ - ' docs/**'
13+ - ' *.md'
14+ workflow_dispatch :
15+
16+ jobs :
17+ build-android :
18+ runs-on : ubuntu-latest
19+
20+ steps :
21+ - name : Checkout repository
22+ uses : actions/checkout@v4
23+
24+ - name : Setup Node.js
25+ uses : actions/setup-node@v4
26+ with :
27+ node-version : ' 20.x'
28+ cache : ' yarn'
29+
30+ - name : Cache Node modules
31+ uses : actions/cache@v4
32+ with :
33+ path : |
34+ node_modules
35+ ~/.yarn/cache
36+ key : ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
37+ restore-keys : |
38+ ${{ runner.os }}-yarn-
39+
40+ - name : Install dependencies
41+ run : yarn install --frozen-lockfile
42+
43+ - name : Build web application
44+ run : yarn build
45+
46+ - name : Initialize Capacitor (if not exists)
47+ run : |
48+ # Check if Capacitor is already configured by looking for config file
49+ CAPACITOR_CONFIG=""
50+ if [ -f "capacitor.config.json" ]; then
51+ CAPACITOR_CONFIG="capacitor.config.json"
52+ elif [ -f "capacitor.config.ts" ]; then
53+ CAPACITOR_CONFIG="capacitor.config.ts"
54+ fi
55+
56+ # Initialize only if no config exists AND no android directory
57+ if [ -z "$CAPACITOR_CONFIG" ] && [ ! -d "android" ]; then
58+ echo "No Capacitor configuration found, initializing..."
59+ npx cap init "SVMSeek Wallet" "com.svmseek.wallet" --web-dir=build
60+ else
61+ echo "Capacitor already configured with: ${CAPACITOR_CONFIG:-android directory}"
62+ fi
63+
64+ - name : Add Android platform (if not exists)
65+ run : |
66+ if [ ! -d "android" ]; then
67+ npx cap add android
68+ fi
69+
70+ - name : Setup Java JDK
71+ uses : actions/setup-java@v4
72+ with :
73+ distribution : ' temurin'
74+ java-version : ' 17'
75+
76+ - name : Setup Android SDK
77+ uses : android-actions/setup-android@v3
78+ with :
79+ api-level : 34
80+ build-tools : 34.0.0
81+
82+ - name : Cache Gradle packages
83+ uses : actions/cache@v4
84+ if : hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') != ''
85+ with :
86+ path : |
87+ ~/.gradle/caches
88+ ~/.gradle/wrapper
89+ android/.gradle
90+ android/app/.gradle
91+ key : ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') }}
92+ restore-keys : |
93+ ${{ runner.os }}-gradle-
94+
95+ - name : Sync Capacitor
96+ run : npx cap sync android
97+
98+ - name : Grant execute permission for gradlew
99+ run : chmod +x android/gradlew
100+
101+ - name : Clean Android project
102+ working-directory : ./android
103+ run : |
104+ # Clean project to remove any corrupted resources or build artifacts
105+ ./gradlew clean --stacktrace
106+
107+ - name : Build Android APK
108+ working-directory : ./android
109+ run : |
110+ # Configure Gradle for faster builds
111+ export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4096m -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=true -Dorg.gradle.caching=true"
112+ ./gradlew assembleDebug --stacktrace --build-cache --parallel --configure-on-demand
113+
114+ - name : Setup Production Keystore (if secrets available)
115+ if : github.ref == 'refs/heads/main' && github.event_name == 'push'
116+ working-directory : ./android
117+ run : |
118+ # Use environment variable instead of direct secret interpolation in shell
119+ if [ ! -z "$ANDROID_KEYSTORE_BASE64" ]; then
120+ echo "Setting up production keystore..."
121+ # Decode safely with error handling
122+ if echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > release.keystore 2>/dev/null; then
123+ echo "Production keystore created successfully"
124+ else
125+ echo "ERROR: Failed to decode keystore base64 data"
126+ exit 1
127+ fi
128+ else
129+ echo "No production keystore secrets found, skipping production build"
130+ fi
131+ env :
132+ ANDROID_KEYSTORE_BASE64 : ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
133+
134+ - name : Build and Sign Production APK (if production keystore available)
135+ if : github.ref == 'refs/heads/main' && github.event_name == 'push'
136+ working-directory : ./android
137+ run : |
138+ if [ -f "release.keystore" ]; then
139+ echo "Building and signing production APK..."
140+
141+ # Clean before production build to prevent resource corruption
142+ ./gradlew clean --stacktrace
143+
144+ # Configure signing in gradle.properties for production build
145+ echo "MYAPP_UPLOAD_STORE_FILE=../release.keystore" >> gradle.properties
146+ echo "MYAPP_UPLOAD_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> gradle.properties
147+ echo "MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> gradle.properties
148+ echo "MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> gradle.properties
149+
150+ # Build signed release APK directly (Gradle handles signing automatically)
151+ export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4096m -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=true -Dorg.gradle.caching=true"
152+ ./gradlew assembleRelease --stacktrace --build-cache --parallel --configure-on-demand
153+
154+ # Verify the signed APK exists and is properly signed with production-grade validation
155+ if [ -f "app/build/outputs/apk/release/app-release.apk" ]; then
156+ echo "Production APK built successfully, performing comprehensive signature validation..."
157+
158+ # 1. Check APK structure integrity
159+ APK_FILE="app/build/outputs/apk/release/app-release.apk"
160+
161+ # 2. Verify APK can be read without corruption
162+ if ! unzip -t "$APK_FILE" > /dev/null 2>&1; then
163+ echo "ERROR: APK file is corrupted or invalid"
164+ exit 1
165+ fi
166+
167+ # 3. Check for proper signature files (META-INF directory)
168+ SIGNATURE_FILES=$(unzip -l "$APK_FILE" | grep -E "META-INF.*\.(RSA|DSA|EC)" | wc -l)
169+ if [ "$SIGNATURE_FILES" -eq 0 ]; then
170+ echo "ERROR: No signature files found in APK"
171+ exit 1
172+ fi
173+ echo "Found $SIGNATURE_FILES signature file(s)"
174+
175+ # 4. Verify APK signature using jarsigner (more thorough than grep)
176+ if command -v jarsigner >/dev/null 2>&1; then
177+ echo "Verifying APK signature with jarsigner..."
178+ if jarsigner -verify -verbose "$APK_FILE" > /tmp/apk_verify.log 2>&1; then
179+ echo "✓ APK signature verification passed"
180+ # Check for timestamp and certificate details
181+ if grep -q "jar verified" /tmp/apk_verify.log; then
182+ echo "✓ APK is properly signed and verified"
183+ else
184+ echo "WARNING: APK verification completed but with potential issues"
185+ cat /tmp/apk_verify.log
186+ fi
187+ else
188+ echo "ERROR: APK signature verification failed"
189+ cat /tmp/apk_verify.log
190+ exit 1
191+ fi
192+ else
193+ echo "WARNING: jarsigner not available, using basic signature check"
194+ unzip -l "$APK_FILE" | grep -E "META-INF.*\.(RSA|DSA|EC)" && echo "✓ Basic signature files present" || (echo "ERROR: No signature files found" && exit 1)
195+ fi
196+
197+ # 5. Check APK size is reasonable (not empty or suspiciously small)
198+ echo "Validating APK size..."
199+ if ! ./scripts/check-apk-size.sh "$APK_FILE" 1; then
200+ echo "ERROR: APK size validation failed"
201+ exit 1
202+ fi
203+
204+ # 6. Verify APK contains expected Android components
205+ REQUIRED_FILES="AndroidManifest.xml classes.dex resources.arsc"
206+ for required_file in $REQUIRED_FILES; do
207+ if ! unzip -l "$APK_FILE" | grep -q "$required_file"; then
208+ echo "ERROR: Required Android component '$required_file' not found in APK"
209+ exit 1
210+ fi
211+ done
212+ echo "✓ All required Android components present"
213+
214+ echo "🎉 Production APK passed comprehensive validation"
215+ else
216+ echo "Production APK not found after build"
217+ ls -la app/build/outputs/apk/release/ || echo "Release output directory not found"
218+ exit 1
219+ fi
220+ else
221+ echo "No production keystore found, skipping production build"
222+ fi
223+ env :
224+ ANDROID_KEYSTORE_PASSWORD : ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
225+ ANDROID_KEY_PASSWORD : ${{ secrets.ANDROID_KEY_PASSWORD }}
226+ ANDROID_KEY_ALIAS : ${{ secrets.ANDROID_KEY_ALIAS }}
227+
228+ - name : Verify APKs
229+ working-directory : ./android
230+ run : |
231+ echo "Verifying debug APK..."
232+ if [ -f "app/build/outputs/apk/debug/app-debug.apk" ]; then
233+ # Basic APK structure validation
234+ unzip -l app/build/outputs/apk/debug/app-debug.apk > /dev/null && echo "Debug APK structure is valid"
235+ echo "Debug APK verified successfully"
236+ else
237+ echo "Debug APK not found for verification"
238+ ls -la app/build/outputs/apk/debug/ || echo "Debug output directory not found"
239+ exit 1
240+ fi
241+
242+ # Verify production APK if it exists
243+ if [ -f "app/build/outputs/apk/release/app-release.apk" ]; then
244+ echo "Verifying production APK..."
245+ unzip -l app/build/outputs/apk/release/app-release.apk > /dev/null && echo "Production APK structure is valid"
246+ echo "Production APK verified successfully"
247+ fi
248+
249+ - name : Rename APK with version
250+ run : |
251+ VERSION=$(node -p "require('./package.json').version")
252+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
253+
254+ # Copy debug APK (always available)
255+ cp android/app/build/outputs/apk/debug/app-debug.apk svmseek-wallet-debug-v${VERSION}-${TIMESTAMP}.apk
256+
257+ # Copy production APK if available
258+ if [ -f "android/app/build/outputs/apk/release/app-release.apk" ]; then
259+ cp android/app/build/outputs/apk/release/app-release.apk svmseek-wallet-production-v${VERSION}-${TIMESTAMP}.apk
260+ echo "Production APK renamed: svmseek-wallet-production-v${VERSION}-${TIMESTAMP}.apk"
261+ fi
262+
263+ echo "Debug APK renamed: svmseek-wallet-debug-v${VERSION}-${TIMESTAMP}.apk"
264+
265+ - name : Upload APK artifact
266+ uses : actions/upload-artifact@v4
267+ with :
268+ name : svmseek-wallet-apk
269+ path : svmseek-wallet-*.apk
270+ retention-days : 30
271+
272+ - name : Upload Production APK as release asset (on main branch)
273+ if : github.ref == 'refs/heads/main' && github.event_name == 'push'
274+ uses : actions/upload-artifact@v4
275+ with :
276+ name : svmseek-wallet-production-apk
277+ path : svmseek-wallet-production-*.apk
278+ retention-days : 90
279+ if-no-files-found : ignore
0 commit comments