Skip to content

Commit 282a1f6

Browse files
fix: remove ephemeral debug keystore signing, require consistent keystore secret
The "Verify and sign APK if needed" step generated a fresh debug keystore on every CI run. Since GitHub Actions runners are ephemeral, each build produced a different signing certificate, causing INSTALL_FAILED_UPDATE_INCOMPATIBLE when trying to update. Changes: - release.yml: Remove ephemeral debug keystore fallback, require ANDROID_KEYSTORE_BASE64 secret, single signed build path, add signature verification step - generate-keystore.yml: New helper workflow to create a signing keystore and provide setup instructions for repository secrets Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/df8487e1-1028-44d9-adc9-42e5e885e5a3 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent effc01e commit 282a1f6

2 files changed

Lines changed: 148 additions & 69 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Generate Android Signing Keystore
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
key_password:
7+
description: "Password for the keystore and key (min 6 characters)"
8+
required: true
9+
type: string
10+
key_alias:
11+
description: "Key alias name"
12+
required: false
13+
default: "osu-release"
14+
type: string
15+
validity_days:
16+
description: "Certificate validity in days"
17+
required: false
18+
default: "10000"
19+
type: string
20+
21+
jobs:
22+
generate:
23+
name: Generate Keystore
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Validate inputs
27+
run: |
28+
if [ ${#KEY_PASSWORD} -lt 6 ]; then
29+
echo "::error::Password must be at least 6 characters."
30+
exit 1
31+
fi
32+
env:
33+
KEY_PASSWORD: ${{ inputs.key_password }}
34+
35+
- name: Generate keystore
36+
run: |
37+
keytool -genkeypair -v \
38+
-keystore osu-release.keystore \
39+
-storepass "$KEY_PASSWORD" \
40+
-keypass "$KEY_PASSWORD" \
41+
-alias "$KEY_ALIAS" \
42+
-keyalg RSA -keysize 2048 \
43+
-validity "$VALIDITY_DAYS" \
44+
-dname "CN=osu! Android Release,O=osu,C=US"
45+
46+
echo "Keystore generated successfully ✓"
47+
keytool -list -v -keystore osu-release.keystore -storepass "$KEY_PASSWORD" | head -20
48+
env:
49+
KEY_PASSWORD: ${{ inputs.key_password }}
50+
KEY_ALIAS: ${{ inputs.key_alias }}
51+
VALIDITY_DAYS: ${{ inputs.validity_days }}
52+
53+
- name: Encode keystore as base64
54+
id: encode
55+
run: |
56+
B64=$(base64 -w 0 osu-release.keystore)
57+
# Write to a file for the summary (avoid exposing in logs)
58+
echo "$B64" > keystore-base64.txt
59+
echo "encoded=true" >> "$GITHUB_OUTPUT"
60+
61+
- name: Upload keystore artifact
62+
uses: actions/upload-artifact@v7
63+
with:
64+
name: osu-signing-keystore
65+
path: osu-release.keystore
66+
retention-days: 1
67+
68+
- name: Upload base64 artifact
69+
uses: actions/upload-artifact@v7
70+
with:
71+
name: osu-signing-keystore-base64
72+
path: keystore-base64.txt
73+
retention-days: 1
74+
75+
- name: Output setup instructions
76+
run: |
77+
echo ""
78+
echo "==================================================================="
79+
echo " KEYSTORE GENERATED — SAVE THESE SECRETS NOW"
80+
echo "==================================================================="
81+
echo ""
82+
echo " Go to: Settings → Secrets and variables → Actions → New repository secret"
83+
echo ""
84+
echo " Add these four secrets:"
85+
echo ""
86+
echo " 1. ANDROID_KEYSTORE_BASE64"
87+
echo " → Value: contents of the 'keystore-base64.txt' file from the"
88+
echo " 'osu-signing-keystore-base64' artifact (download it above)"
89+
echo ""
90+
echo " 2. ANDROID_SIGNING_KEY_ALIAS"
91+
echo " → Value: ${{ inputs.key_alias }}"
92+
echo ""
93+
echo " 3. ANDROID_SIGNING_KEY_PASSWORD"
94+
echo " → Value: the password you entered when triggering this workflow"
95+
echo ""
96+
echo " 4. ANDROID_SIGNING_STORE_PASSWORD"
97+
echo " → Value: the password you entered when triggering this workflow"
98+
echo ""
99+
echo " IMPORTANT: Keep a backup of the keystore file! If you lose it,"
100+
echo " you will never be able to update your installed APK — you would"
101+
echo " have to uninstall and reinstall (losing all local data)."
102+
echo ""
103+
echo " After saving the secrets, run the 'Build Android APK' workflow."
104+
echo "==================================================================="

.github/workflows/release.yml

Lines changed: 44 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,38 @@ jobs:
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

Comments
 (0)