@@ -66,49 +66,70 @@ jobs:
6666 echo "Native libraries built successfully:"
6767 find osu.Android/libs -name "*.so" -exec ls -lh {} \;
6868
69- - name : Decode keystore
69+ - name : Decode or generate keystore
7070 id : keystore
7171 run : |
72+ KS_PATH="${{ github.workspace }}/osu.Android/osu.keystore"
73+
7274 if [ -n "$KEYSTORE_BASE64" ]; then
73- echo "$KEYSTORE_BASE64" | base64 --decode > "${{ github.workspace }}/osu.Android/osu.keystore"
74- echo "has_keystore=true" >> "$GITHUB_OUTPUT"
75+ # ── User provided a persistent keystore secret ──────────────
76+ echo "$KEYSTORE_BASE64" | base64 --decode > "$KS_PATH"
77+ echo "has_keystore=true" >> "$GITHUB_OUTPUT"
78+ echo "generated=false" >> "$GITHUB_OUTPUT"
79+ echo "key_alias=$KEY_ALIAS_SECRET" >> "$GITHUB_OUTPUT"
80+ echo "key_pass=$KEY_PASS_SECRET" >> "$GITHUB_OUTPUT"
81+ echo "store_pass=$STORE_PASS_SECRET" >> "$GITHUB_OUTPUT"
82+ echo "✅ Using saved keystore from repository secrets."
7583 else
76- echo "has_keystore=false" >> "$GITHUB_OUTPUT"
84+ # ── No secret → auto-generate a keystore for this build ─────
85+ # The APK will install fine on any device, but UPDATING from a
86+ # previous build signed with a DIFFERENT key will fail.
87+ # To avoid that, save the generated keystore as a secret
88+ # (instructions are printed at the end of the build).
89+ AUTO_PASS="osu-$(openssl rand -hex 12)"
90+ echo "::add-mask::$AUTO_PASS"
91+
92+ keytool -genkeypair \
93+ -keystore "$KS_PATH" \
94+ -storepass "$AUTO_PASS" \
95+ -keypass "$AUTO_PASS" \
96+ -alias osu-release \
97+ -keyalg RSA -keysize 2048 -validity 10000 \
98+ -dname "CN=osu! Android,O=osu,C=US" 2>/dev/null
99+
100+ echo "has_keystore=true" >> "$GITHUB_OUTPUT"
101+ echo "generated=true" >> "$GITHUB_OUTPUT"
102+ echo "key_alias=osu-release" >> "$GITHUB_OUTPUT"
103+ echo "key_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT"
104+ echo "store_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT"
105+
106+ # Export base64 and password into a single instructions file
107+ # so the user only needs to download one artifact.
108+ {
109+ echo "=== osu! Android Signing Keystore ==="
110+ echo ""
111+ echo "ANDROID_KEYSTORE_BASE64 value (copy everything on the next line):"
112+ base64 -w 0 "$KS_PATH"
113+ echo ""
114+ echo ""
115+ echo "ANDROID_SIGNING_KEY_ALIAS value:"
116+ echo "osu-release"
117+ echo ""
118+ echo "ANDROID_SIGNING_KEY_PASSWORD value:"
119+ echo "$AUTO_PASS"
120+ echo ""
121+ echo "ANDROID_SIGNING_STORE_PASSWORD value:"
122+ echo "$AUTO_PASS"
123+ } > "${{ github.workspace }}/SAVE-THESE-SECRETS.txt"
124+
125+ echo "::warning::No signing keystore secret found — auto-generated one for this build."
126+ echo "::warning::See the end of this job for instructions to save it for future builds."
77127 fi
78128 env :
79129 KEYSTORE_BASE64 : ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
80-
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
130+ KEY_ALIAS_SECRET : ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
131+ KEY_PASS_SECRET : ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
132+ STORE_PASS_SECRET : ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
112133
113134 - name : Set version
114135 id : version
@@ -121,10 +142,6 @@ jobs:
121142 fi
122143
123144 - name : Build Android APK
124- env :
125- ANDROID_KEY_ALIAS : ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
126- ANDROID_KEY_PASS : ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
127- ANDROID_STORE_PASS : ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
128145 run : >
129146 dotnet publish -c Release
130147 osu.Android/osu.Android.csproj
@@ -134,9 +151,9 @@ jobs:
134151 -p:ApplicationVersion="${{ github.run_number }}"
135152 -p:AndroidKeyStore=true
136153 -p:AndroidSigningKeyStore="${{ github.workspace }}/osu.Android/osu.keystore"
137- -p:AndroidSigningKeyAlias="$ANDROID_KEY_ALIAS "
138- -p:AndroidSigningKeyPass="$ANDROID_KEY_PASS "
139- -p:AndroidSigningStorePass="$ANDROID_STORE_PASS "
154+ -p:AndroidSigningKeyAlias="${{ steps.keystore.outputs.key_alias }} "
155+ -p:AndroidSigningKeyPass="${{ steps.keystore.outputs.key_pass }} "
156+ -p:AndroidSigningStorePass="${{ steps.keystore.outputs.store_pass }} "
140157 -p:CustomBeforeMicrosoftCommonTargets="${{ github.workspace }}/build/SuppressSubmoduleWarnings.targets"
141158
142159 - name : Find APK
@@ -177,6 +194,47 @@ jobs:
177194 path : ${{ steps.find_apk.outputs.apk_path }}
178195 if-no-files-found : error
179196
197+ # When the keystore was auto-generated, upload it so the user can save it
198+ # as a repository secret for consistent signing across builds.
199+ - name : Upload generated keystore
200+ if : steps.keystore.outputs.generated == 'true'
201+ uses : actions/upload-artifact@v7
202+ with :
203+ name : osu-signing-keystore
204+ path : |
205+ ${{ github.workspace }}/osu.Android/osu.keystore
206+ ${{ github.workspace }}/SAVE-THESE-SECRETS.txt
207+ retention-days : 7
208+
209+ - name : Print keystore setup instructions
210+ if : steps.keystore.outputs.generated == 'true'
211+ run : |
212+ echo ""
213+ echo "=================================================================="
214+ echo " ⚠️ YOUR APK WAS SIGNED WITH AN AUTO-GENERATED KEYSTORE"
215+ echo "=================================================================="
216+ echo ""
217+ echo " ✅ The APK will install fine on any device."
218+ echo ""
219+ echo " ⚠️ BUT — if you build again without saving this keystore,"
220+ echo " Android will REFUSE to update (different signing certificate)."
221+ echo ""
222+ echo " To keep your APK updatable across builds:"
223+ echo ""
224+ echo " 1. Download the 'osu-signing-keystore' artifact from this run"
225+ echo " 2. Open 'SAVE-THESE-SECRETS.txt' — it contains all 4 values"
226+ echo " 3. Go to: Settings → Secrets and variables → Actions"
227+ echo " 4. Create these 4 secrets with the values from the file:"
228+ echo ""
229+ echo " • ANDROID_KEYSTORE_BASE64"
230+ echo " • ANDROID_SIGNING_KEY_ALIAS"
231+ echo " • ANDROID_SIGNING_KEY_PASSWORD"
232+ echo " • ANDROID_SIGNING_STORE_PASSWORD"
233+ echo ""
234+ echo " ⚡ After saving the secrets, all future builds will use the"
235+ echo " same keystore automatically — no more setup needed."
236+ echo "=================================================================="
237+
180238 - name : Create GitHub Release
181239 if : startsWith(github.ref, 'refs/tags/')
182240 uses : softprops/action-gh-release@v2
0 commit comments