7878 env :
7979 KEYSTORE_BASE64 : ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
8080
81+ # Signing is REQUIRED for installable APKs. Without a consistent keystore
82+ # across builds Android will reject updates with INSTALL_FAILED_UPDATE_INCOMPATIBLE.
83+ # The old fallback (generating a fresh debug keystore each CI run) produced a
84+ # different certificate every time — making every APK incompatible with the last.
85+ #
86+ # To set up signing, run the "Generate Android Signing Keystore" workflow
87+ # (generate-keystore.yml) and save the four secrets it outputs.
88+ - name : Require signing keystore
89+ if : steps.keystore.outputs.has_keystore != 'true'
90+ run : |
91+ echo "::error::No signing keystore configured. APK builds require a consistent signing key."
92+ echo ""
93+ echo "==================================================================="
94+ echo " SETUP INSTRUCTIONS"
95+ echo "==================================================================="
96+ echo ""
97+ echo " 1. Go to Actions → 'Generate Android Signing Keystore' → Run workflow"
98+ echo " 2. Download the keystore artifact and copy the base64 from the job summary"
99+ echo " 3. Add these four repository secrets (Settings → Secrets → Actions):"
100+ echo ""
101+ echo " ANDROID_KEYSTORE_BASE64 = <base64 from step 2>"
102+ echo " ANDROID_SIGNING_KEY_ALIAS = osu-release"
103+ echo " ANDROID_SIGNING_KEY_PASSWORD = <password you chose>"
104+ echo " ANDROID_SIGNING_STORE_PASSWORD = <password you chose>"
105+ echo ""
106+ echo " 4. Re-run this workflow."
107+ echo ""
108+ echo " Why? Each CI run is ephemeral. Without a stored keystore, every build"
109+ echo " gets a different signing certificate, causing INSTALL_FAILED_UPDATE_INCOMPATIBLE."
110+ echo "==================================================================="
111+ exit 1
112+
81113 - name : Set version
82114 id : version
83115 run : |
@@ -88,11 +120,7 @@ jobs:
88120 echo "version=0.0.0" >> "$GITHUB_OUTPUT"
89121 fi
90122
91- # Always build Release for full optimization (trimming, AOT, compression).
92- # When a keystore is available we sign with it; otherwise the SDK produces
93- # a debug-signed Release APK that can be sideloaded for testing.
94- - name : Build Android APK (signed)
95- if : steps.keystore.outputs.has_keystore == 'true'
123+ - name : Build Android APK
96124 env :
97125 ANDROID_KEY_ALIAS : ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
98126 ANDROID_KEY_PASS : ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
@@ -111,32 +139,16 @@ jobs:
111139 -p:AndroidSigningStorePass="$ANDROID_STORE_PASS"
112140 -p:CustomBeforeMicrosoftCommonTargets="${{ github.workspace }}/build/SuppressSubmoduleWarnings.targets"
113141
114- - name : Build Android APK (unsigned Release)
115- if : steps.keystore.outputs.has_keystore != 'true'
116- run : >
117- dotnet publish -c Release
118- osu.Android/osu.Android.csproj
119- -f net10.0-android
120- -p:Version="${{ steps.version.outputs.version }}"
121- -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}"
122- -p:ApplicationVersion="${{ github.run_number }}"
123- -p:CustomBeforeMicrosoftCommonTargets="${{ github.workspace }}/build/SuppressSubmoduleWarnings.targets"
124-
125142 - name : Find APK
126143 id : find_apk
127144 run : |
128- # Both paths build Release. Signed builds produce *-Signed.apk; unsigned
129- # builds produce the base APK name. Search publish dir first, then fallback.
130- if [ "${{ steps.keystore.outputs.has_keystore }}" == "true" ]; then
131- APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*-Signed.apk" 2>/dev/null | head -1)
132- if [ -z "$APK" ]; then
133- APK=$(find "osu.Android/bin/Release" -name "*-Signed.apk" | head -1)
134- fi
135- else
145+ APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*-Signed.apk" 2>/dev/null | head -1)
146+ if [ -z "$APK" ]; then
147+ APK=$(find "osu.Android/bin/Release" -name "*-Signed.apk" | head -1)
148+ fi
149+ if [ -z "$APK" ]; then
150+ # Fall back to any APK if -Signed variant isn't produced
136151 APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)
137- if [ -z "$APK" ]; then
138- APK=$(find "osu.Android/bin/Release" -name "*.apk" | head -1)
139- fi
140152 fi
141153
142154 if [ -z "$APK" ]; then
@@ -150,53 +162,16 @@ jobs:
150162 echo "Found APK: $APK ($APK_SIZE_MB MB)"
151163 echo "apk_path=$APK" >> "$GITHUB_OUTPUT"
152164
153- # .NET 10 Android SDK may skip debug-signing for Release publish builds.
154- # Verify the APK is signed; if not, sign it with apksigner using the debug
155- # keystore so the APK can be sideloaded without INSTALL_PARSE_FAILED_NO_CERTIFICATES.
156- - name : Verify and sign APK if needed
165+ - name : Verify APK signature
157166 run : |
158167 APK="${{ steps.find_apk.outputs.apk_path }}"
159168 APKSIGNER="$ANDROID_HOME/build-tools/$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1)/apksigner"
160- ZIPALIGN="$ANDROID_HOME/build-tools/$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1)/zipalign"
161169
162- if "$APKSIGNER" verify "$APK" 2>/dev/null ; then
163- echo "APK is already signed ✓"
170+ if "$APKSIGNER" verify --print-certs "$APK"; then
171+ echo "APK signature verified ✓"
164172 else
165- echo "::warning::APK is not signed. Signing with debug keystore..."
166-
167- # Generate debug keystore if it doesn't exist
168- DEBUG_KS="$HOME/.android/debug.keystore"
169- if [ ! -f "$DEBUG_KS" ]; then
170- mkdir -p "$HOME/.android"
171- keytool -genkeypair -v \
172- -keystore "$DEBUG_KS" \
173- -storepass android \
174- -keypass android \
175- -alias androiddebugkey \
176- -keyalg RSA -keysize 2048 -validity 10000 \
177- -dname "CN=Android Debug,O=Android,C=US"
178- fi
179-
180- # Zipalign first (required before apksigner v2 signing)
181- ALIGNED_APK="${APK%.apk}-aligned.apk"
182- "$ZIPALIGN" -f -p 4 "$APK" "$ALIGNED_APK"
183- mv "$ALIGNED_APK" "$APK"
184-
185- # Sign with debug keystore (v1 + v2 + v3 schemes)
186- "$APKSIGNER" sign \
187- --ks "$DEBUG_KS" \
188- --ks-pass pass:android \
189- --key-pass pass:android \
190- --ks-key-alias androiddebugkey \
191- "$APK"
192-
193- # Verify signature
194- if "$APKSIGNER" verify --print-certs "$APK"; then
195- echo "APK signed successfully ✓"
196- else
197- echo "::error::APK signing failed"
198- exit 1
199- fi
173+ echo "::error::APK signature verification failed. The APK may not install correctly."
174+ exit 1
200175 fi
201176
202177 - name : Upload APK artifact
0 commit comments