1- name : Build Signed Android APK/AAB
2-
3- on :
4- workflow_dispatch :
5- inputs :
6- build_type :
7- description : " Build type"
8- required : true
9- default : " release"
10- type : choice
11- options :
12- - debug
13- - release
14- push :
15- tags :
16- - " v*.*.*"
17-
181jobs :
192 build-android :
203 runs-on : ubuntu-latest
21-
4+
225 steps :
236 - name : Checkout code
247 uses : actions/checkout@v4
258
269 - name : Set up Node.js
2710 uses : actions/setup-node@v4
2811 with :
29- node-version : " 20 "
30- cache : " npm"
12+ node-version : ' 20 '
13+ cache : ' npm'
3114
3215 - name : Set up JDK 17
3316 uses : actions/setup-java@v4
3417 with :
35- distribution : " temurin"
36- java-version : " 17"
18+ distribution : ' temurin'
19+ java-version : ' 17'
20+
21+ - name : Setup Android SDK
22+ uses : android-actions/setup-android@v3
3723
3824 - name : Install dependencies
3925 run : npm install --legacy-peer-deps
@@ -42,217 +28,98 @@ jobs:
4228 run : npx expo prebuild --platform android --clean
4329
4430 - name : Setup Gradle cache
45- uses : gradle/actions/setup-gradle@v4
31+ uses : gradle/actions/setup-gradle@v3
4632 with :
4733 gradle-home-cache-cleanup : true
4834
4935 - name : Decode Keystore
5036 run : |
51- mkdir -p android/app
52- echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/release.keystore
53- ls -lh android/app/release.keystore
54- echo "✅ Keystore decoded"
55-
56- - name : Setup signing in gradle.properties
57- run : |
58- echo "📝 Setting up gradle.properties..."
59- cat >> android/gradle.properties << EOF
60- MYAPP_RELEASE_STORE_FILE=release.keystore
61- MYAPP_RELEASE_KEY_ALIAS=${{ secrets.KEY_ALIAS }}
62- MYAPP_RELEASE_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}
63- MYAPP_RELEASE_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}
64- EOF
65-
66- echo "✅ gradle.properties configured"
67- echo ""
68- echo "📋 Verification (with secrets masked):"
69- cat android/gradle.properties | grep MYAPP_RELEASE_STORE_FILE
70- echo "MYAPP_RELEASE_KEY_ALIAS=***"
71- echo "MYAPP_RELEASE_STORE_PASSWORD=***"
72- echo "MYAPP_RELEASE_KEY_PASSWORD=***"
73-
74- - name : Configure build.gradle for signing
75- run : |
76- BUILD_GRADLE="android/app/build.gradle"
77- cp "$BUILD_GRADLE" "${BUILD_GRADLE}.backup"
78-
79- echo "📝 Adding release signing config..."
80-
81- # Add release signing config to signingConfigs block
82- awk '
83- /signingConfigs \{/ {
84- print
85- print " release {"
86- print " def propsFile = rootProject.file(\"gradle.properties\")"
87- print " if (propsFile.exists()) {"
88- print " def props = new Properties()"
89- print " propsFile.withInputStream { props.load(it) }"
90- print " storeFile file(props[\"MYAPP_RELEASE_STORE_FILE\"])"
91- print " storePassword props[\"MYAPP_RELEASE_STORE_PASSWORD\"]"
92- print " keyAlias props[\"MYAPP_RELEASE_KEY_ALIAS\"]"
93- print " keyPassword props[\"MYAPP_RELEASE_KEY_PASSWORD\"]"
94- print " }"
95- print " }"
96- next
97- }
98- { print }
99- ' "$BUILD_GRADLE" > "${BUILD_GRADLE}.tmp"
100- mv "${BUILD_GRADLE}.tmp" "$BUILD_GRADLE"
101-
102- echo "📝 Removing conflicting debug signing from release buildType..."
103-
104- # Remove ALL signingConfig lines from release buildType, then add back the correct one
105- sed -i '/buildTypes {/,/^ }$/ {
106- /release {/,/^ }$/ {
107- /signingConfig/d
108- }
109- }' "$BUILD_GRADLE"
110-
111- # Add the correct signingConfig at the start of release block
112- sed -i '/buildTypes {/,/^ }$/ {
113- /release {/a\
114- signingConfig signingConfigs.release
115- }' "$BUILD_GRADLE"
116-
117- echo "✅ Configuration complete"
118- echo ""
119- echo "📋 Final configuration:"
120- grep -B 2 -A 30 "signingConfigs {" "$BUILD_GRADLE"
121- echo ""
122- echo "📋 Release buildType:"
123- grep -A 15 "release {" "$BUILD_GRADLE" | head -20
37+ echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > release.keystore
38+ echo "✅ Keystore decoded to release.keystore"
12439
12540 - name : Make gradlew executable
12641 run : chmod +x android/gradlew
12742
128- - name : Clean previous builds
129- run : cd android && ./gradlew clean
130-
131- - name : Verify gradle.properties before build
43+ # Build the UNsigned Release APK
44+ - name : Build Release APK (Unsigned)
13245 run : |
133- echo "📋 Checking gradle.properties..."
134- cat android/gradle.properties | grep MYAPP_ || echo "⚠️ Properties not found in gradle.properties"
135-
136- echo ""
137- echo "📋 Checking if keystore exists..."
138- ls -lh android/app/release.keystore || echo "❌ Keystore not found!"
139-
140- echo ""
141- echo "📋 Testing property access..."
14246 cd android
143- ./gradlew properties | grep MYAPP_ || echo "⚠️ Gradle cannot see MYAPP properties"
47+ ./gradlew assembleRelease --no-daemon
14448
145- - name : Build Signed APK with explicit signing
49+ # Sign the APK manually using apksigner
50+ - name : Sign APK
51+ id : sign_apk
52+ run : |
53+ # Find the unsigned APK
54+ APK_PATH=$(find android/app/build/outputs/apk/release -name "*-release-unsigned.apk" -o -name
55+ "*-release.apk" | head -1)
56+ echo "Found APK: $APK_PATH"
57+
58+ # Align the APK (Optimization step required before signing)
59+ $ANDROID_HOME/build-tools/34.0.0/zipalign -v -p 4 "$APK_PATH" release-aligned.apk
60+
61+ # Sign the APK
62+ $ANDROID_HOME/build-tools/34.0.0/apksigner sign --ks release.keystore \
63+ --ks-key-alias "${{ secrets.KEY_ALIAS }}" \
64+ --ks-pass "pass:${{ secrets.KEYSTORE_PASSWORD }}" \
65+ --key-pass "pass:${{ secrets.KEY_PASSWORD }}" \
66+ --out release-signed.apk \
67+ release-aligned.apk
68+
69+ echo "✅ APK Signed successfully"
70+ echo "signed_apk_path=release-signed.apk" >> $GITHUB_OUTPUT
71+
72+ # Build the AAB (Android App Bundle)
73+ - name : Build Release AAB
14674 run : |
14775 cd android
76+ ./gradlew bundleRelease --no-daemon
14877
149- echo "🔨 Building with signing..."
150- ./gradlew assembleRelease \
151- -PMYAPP_RELEASE_STORE_FILE=release.keystore \
152- -PMYAPP_RELEASE_KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
153- -PMYAPP_RELEASE_STORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \
154- -PMYAPP_RELEASE_KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
155- --no-daemon \
156- --stacktrace \
157- --info 2>&1 | tee build.log
158-
159- echo ""
160- echo "📋 Checking build log for signing..."
161- if grep -i "signing" build.log | grep -i "release"; then
162- echo "✅ Signing configuration was used"
163- else
164- echo "⚠️ Warning: No signing activity detected"
165- fi
166-
167- - name : Build Signed AAB with explicit signing
78+ # Sign the AAB manually using jarsigner
79+ - name : Sign AAB
16880 run : |
169- cd android
170- ./gradlew bundleRelease \
171- -PMYAPP_RELEASE_STORE_FILE=release.keystore \
172- -PMYAPP_RELEASE_KEY_ALIAS="${{ secrets.KEY_ALIAS }}" \
173- -PMYAPP_RELEASE_STORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \
174- -PMYAPP_RELEASE_KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}" \
175- --no-daemon \
176- --stacktrace
81+ # Find the AAB
82+ AAB_PATH=$(find android/app/build/outputs/bundle/release -name "*-release.aab" | head -1)
83+ echo "Found AAB: $AAB_PATH"
84+
85+ # Sign using jarsigner (AABs use jarsigner, not apksigner)
86+ jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
87+ -keystore release.keystore \
88+ -storepass "${{ secrets.KEYSTORE_PASSWORD }}" \
89+ -keypass "${{ secrets.KEY_PASSWORD }}" \
90+ "$AAB_PATH" "${{ secrets.KEY_ALIAS }}"
91+
92+ mv "$AAB_PATH" release-signed.aab
93+ echo "✅ AAB Signed successfully"
17794
17895 - name : Verify APK signature
17996 run : |
180- APK_PATH=$(find android/app/build/outputs/apk/release -name "*.apk" | head -1)
181-
182- if [ ! -f "$APK_PATH" ]; then
183- echo "❌ APK not found!"
184- exit 1
185- fi
186-
187- echo "📦 Verifying: $APK_PATH"
188- echo ""
189-
190- # List all META-INF files
191- echo "=== META-INF Directory Contents ==="
192- unzip -l "$APK_PATH" | grep "META-INF/" || {
193- echo "❌ No META-INF directory found - APK is completely unsigned!"
194- exit 1
195- }
196- echo ""
197-
198- # Find certificate file
199- CERT_FILE=$(unzip -l "$APK_PATH" | grep -o "META-INF/[A-Z0-9]*\.RSA" | head -1)
200-
201- if [ -z "$CERT_FILE" ]; then
202- echo "❌ No RSA certificate found in APK!"
203- echo "This means the APK was not signed at all."
204- echo ""
205- echo "DEBUG: Checking build.gradle signing configuration..."
206- grep -A 15 "signingConfigs" android/app/build.gradle
207- exit 1
208- fi
209-
210- echo "=== Certificate Information ==="
211- echo "Certificate file: $CERT_FILE"
212- unzip -p "$APK_PATH" "$CERT_FILE" | keytool -printcert
213-
214- echo ""
215- echo "=== SHA1 Fingerprint ==="
216- SHA1=$(unzip -p "$APK_PATH" "$CERT_FILE" | keytool -printcert | grep "SHA1:" | awk '{print $2}')
217- echo "Current SHA1: $SHA1"
218- echo "Expected SHA1: 59:3D:24:BE:25:91:DF:54:6E:A8:06:17:DC:A1:73:81:3E:71:4C:A0"
219-
220- if [ "$SHA1" == "59:3D:24:BE:25:91:DF:54:6E:A8:06:17:DC:A1:73:81:3E:71:4C:A0" ]; then
221- echo "✅ ✅ ✅ CORRECT KEYSTORE! ✅ ✅ ✅"
222- else
223- echo "⚠️ WARNING: This is a different keystore!"
224- echo "Google Play will reject this upload."
225- fi
97+ echo "📦 Verifying: release-signed.apk"
98+
99+ # Use apksigner verify which is more robust
100+ $ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs release-signed.apk
101+
102+ # Quick check for your specific SHA1 if needed
103+ SHA1=$($ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs release-signed.apk |
104+ grep "SHA-1 digest:" | head -1 | awk '{print $3}')
105+ echo "Detected SHA1: $SHA1"
226106
227107 - name : Upload APK artifact
228108 uses : actions/upload-artifact@v4
229109 with :
230110 name : app-release-apk-${{ github.run_number }}
231- path : android/app/build/outputs/apk/ release/* .apk
111+ path : release-signed .apk
232112 retention-days : 30
233113
234114 - name : Upload AAB artifact
235115 uses : actions/upload-artifact@v4
236116 with :
237117 name : app-release-aab-${{ github.run_number }}
238- path : android/app/build/outputs/bundle/ release/* .aab
118+ path : release-signed .aab
239119 retention-days : 30
240120
241- - name : Upload debug files on failure
242- if : failure()
243- uses : actions/upload-artifact@v4
244- with :
245- name : debug-files-${{ github.run_number }}
246- path : |
247- android/build.log
248- android/app/build.gradle
249- android/app/build.gradle.backup
250- android/gradle.properties
251- retention-days : 7
252-
253121 - name : Cleanup sensitive files
254122 if : always()
255123 run : |
256- rm -f android/app/release.keystore
257- rm -f android/gradle.properties
124+ rm -f release.keystore
258125 echo "✅ Cleanup completed"
0 commit comments