Fix INSTALL_FAILED_UPDATE_INCOMPATIBLE, add auto-signing, versioning, releases, and fork documentation - #205
Conversation
…tore 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>
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/df8487e1-1028-44d9-adc9-42e5e885e5a3 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…de lambda - oboe_bridge.cpp: eagerly compute big-core mask in start() so the audio callback never calls computeBigCoreMask() (which does fopen). Defensive fallback in callback uses upper-half heuristic instead of file I/O if cache is somehow cold. - OsuGameAndroid.cs: wrap Schedule lambda in SelectHighestRefreshRate() with try-catch to prevent unhandled exceptions from stale display references. Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/df8487e1-1028-44d9-adc9-42e5e885e5a3 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…dispatch Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1d3c6971-e9fd-4074-9f8e-0bcb8c85965d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…orkflow Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1d3c6971-e9fd-4074-9f8e-0bcb8c85965d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…ream ppy/osu Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1d3c6971-e9fd-4074-9f8e-0bcb8c85965d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1d3c6971-e9fd-4074-9f8e-0bcb8c85965d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Android fork to improve runtime stability/performance and modernise the release pipeline, alongside a substantial README rewrite documenting fork-specific functionality and CI/CD.
Changes:
- Harden Android runtime behaviour (display mode application safety + avoid sysfs file I/O on the real-time audio callback thread).
- Add/extend Android release workflow features (keystore handling, versioning, APK naming, GitHub Releases).
- Rewrite README to document fork-specific Android/framework/veldrid changes and build instructions.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
osu.Android/OsuGameAndroid.cs |
Wraps scheduled refresh-rate selection/application in exception handling to avoid crashes from stale display state. |
osu.Android/Native/oboe_bridge.cpp |
Precomputes/caches big-core CPU mask in start() and adds an audio-thread fallback that avoids sysfs file I/O. |
README.md |
Replaces upstream boilerplate with fork-specific Android integration, performance, build, and CI/CD documentation. |
.github/workflows/release.yml |
Adds workflow inputs, auto versioning, auto keystore generation, signed APK verification, artifact uploads, and GitHub Release creation. |
.github/workflows/generate-keystore.yml |
Adds a helper workflow to generate and export a signing keystore + setup instructions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if [ -n "$KEYSTORE_BASE64" ]; then | ||
| echo "$KEYSTORE_BASE64" | base64 --decode > "${{ github.workspace }}/osu.Android/osu.keystore" | ||
| echo "has_keystore=true" >> "$GITHUB_OUTPUT" | ||
| # ── User provided a persistent keystore secret ────────────── | ||
| echo "$KEYSTORE_BASE64" | base64 --decode > "$KS_PATH" | ||
| echo "has_keystore=true" >> "$GITHUB_OUTPUT" | ||
| echo "generated=false" >> "$GITHUB_OUTPUT" | ||
| echo "key_alias=$KEY_ALIAS_SECRET" >> "$GITHUB_OUTPUT" | ||
| echo "key_pass=$KEY_PASS_SECRET" >> "$GITHUB_OUTPUT" | ||
| echo "store_pass=$STORE_PASS_SECRET" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
If ANDROID_KEYSTORE_BASE64 is set but one of ANDROID_SIGNING_KEY_ALIAS / ANDROID_SIGNING_KEY_PASSWORD / ANDROID_SIGNING_STORE_PASSWORD is missing or empty, this step will still output has_keystore=true and pass empty values into dotnet publish, causing a confusing signing failure later. Add an explicit validation here (fail early with a clear error) when using a provided keystore secret.
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" | ||
|
|
There was a problem hiding this comment.
tag is always set to v$VERSION, even when this workflow is triggered by an existing tag push (e.g. 1.2.3). That causes the release step to create a new tag, and because the workflow is configured to run on *.*.*, the newly created tag (like v1.2.3) will also trigger this workflow again, potentially leading to a tag/workflow recursion loop. Use the pushed tag name as-is when github.event_name == 'push', and ensure workflow-created tags are excluded from the on.push.tags patterns (or add a job-level guard to skip runs for tags created by github-actions[bot]).
| # When the keystore was auto-generated, upload it so the user can save it | ||
| # as a repository secret for consistent signing across builds. | ||
| - name: Upload generated keystore | ||
| if: steps.keystore.outputs.generated == 'true' | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: osu-signing-keystore | ||
| path: | | ||
| ${{ github.workspace }}/osu.Android/osu.keystore | ||
| ${{ github.workspace }}/SAVE-THESE-SECRETS.txt | ||
| retention-days: 7 | ||
|
|
||
| - name: Print keystore setup instructions | ||
| if: steps.keystore.outputs.generated == 'true' | ||
| run: | | ||
| echo "" | ||
| echo "==================================================================" | ||
| echo " ⚠️ YOUR APK WAS SIGNED WITH AN AUTO-GENERATED KEYSTORE" | ||
| echo "==================================================================" | ||
| echo "" | ||
| echo " ✅ The APK will install fine on any device." | ||
| echo "" | ||
| echo " ⚠️ BUT — if you build again without saving this keystore," | ||
| echo " Android will REFUSE to update (different signing certificate)." | ||
| echo "" | ||
| echo " To keep your APK updatable across builds:" | ||
| echo "" | ||
| echo " 1. Download the 'osu-signing-keystore' artifact from this run" | ||
| echo " 2. Open 'SAVE-THESE-SECRETS.txt' — it contains all 4 values" | ||
| echo " 3. Go to: Settings → Secrets and variables → Actions" | ||
| echo " 4. Create these 4 secrets with the values from the file:" | ||
| echo "" | ||
| echo " • ANDROID_KEYSTORE_BASE64" | ||
| echo " • ANDROID_SIGNING_KEY_ALIAS" | ||
| echo " • ANDROID_SIGNING_KEY_PASSWORD" | ||
| echo " • ANDROID_SIGNING_STORE_PASSWORD" | ||
| echo "" | ||
| echo " ⚡ After saving the secrets, all future builds will use the" | ||
| echo " same keystore automatically — no more setup needed." | ||
| echo "==================================================================" |
There was a problem hiding this comment.
Uploading the generated keystore (and the base64/password instructions) as a workflow artifact exposes the signing key material to anyone with access to Actions artifacts for this repo. If this repository is public (or has many readers), leaking the signing key allows third parties to produce malicious APK updates signed with the same certificate. Consider removing keystore/material upload entirely (fail the build and require secrets), or restrict the workflow to trusted/private contexts where artifacts are not broadly accessible.
| # When the keystore was auto-generated, upload it so the user can save it | |
| # as a repository secret for consistent signing across builds. | |
| - name: Upload generated keystore | |
| if: steps.keystore.outputs.generated == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: osu-signing-keystore | |
| path: | | |
| ${{ github.workspace }}/osu.Android/osu.keystore | |
| ${{ github.workspace }}/SAVE-THESE-SECRETS.txt | |
| retention-days: 7 | |
| - name: Print keystore setup instructions | |
| if: steps.keystore.outputs.generated == 'true' | |
| run: | | |
| echo "" | |
| echo "==================================================================" | |
| echo " ⚠️ YOUR APK WAS SIGNED WITH AN AUTO-GENERATED KEYSTORE" | |
| echo "==================================================================" | |
| echo "" | |
| echo " ✅ The APK will install fine on any device." | |
| echo "" | |
| echo " ⚠️ BUT — if you build again without saving this keystore," | |
| echo " Android will REFUSE to update (different signing certificate)." | |
| echo "" | |
| echo " To keep your APK updatable across builds:" | |
| echo "" | |
| echo " 1. Download the 'osu-signing-keystore' artifact from this run" | |
| echo " 2. Open 'SAVE-THESE-SECRETS.txt' — it contains all 4 values" | |
| echo " 3. Go to: Settings → Secrets and variables → Actions" | |
| echo " 4. Create these 4 secrets with the values from the file:" | |
| echo "" | |
| echo " • ANDROID_KEYSTORE_BASE64" | |
| echo " • ANDROID_SIGNING_KEY_ALIAS" | |
| echo " • ANDROID_SIGNING_KEY_PASSWORD" | |
| echo " • ANDROID_SIGNING_STORE_PASSWORD" | |
| echo "" | |
| echo " ⚡ After saving the secrets, all future builds will use the" | |
| echo " same keystore automatically — no more setup needed." | |
| echo "==================================================================" | |
| # Never export generated signing material via workflow artifacts. | |
| # Require configured signing secrets instead of disclosing the keystore. | |
| - name: Fail if signing secrets are not configured | |
| if: steps.keystore.outputs.generated == 'true' | |
| run: | | |
| echo "::error::Signing secrets are not configured. Refusing to continue because uploading or exposing an auto-generated Android keystore would leak the app signing key material." | |
| echo "::error::Configure ANDROID_KEYSTORE_BASE64, ANDROID_SIGNING_KEY_ALIAS, ANDROID_SIGNING_KEY_PASSWORD, and ANDROID_SIGNING_STORE_PASSWORD as GitHub Actions secrets, then rerun the workflow." | |
| exit 1 |
| - name: Encode keystore as base64 | ||
| id: encode | ||
| run: | | ||
| B64=$(base64 -w 0 osu-release.keystore) | ||
| echo "$B64" > keystore-base64.txt | ||
| echo "encoded=true" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Upload keystore artifact | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: osu-signing-keystore | ||
| path: osu-release.keystore | ||
| retention-days: 1 | ||
|
|
||
| - name: Upload base64 artifact | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: osu-signing-keystore-base64 | ||
| path: keystore-base64.txt | ||
| retention-days: 1 | ||
|
|
||
| - name: Output setup instructions | ||
| run: | | ||
| echo "" | ||
| echo "===================================================================" | ||
| echo " KEYSTORE GENERATED — SAVE THESE SECRETS NOW" | ||
| echo "===================================================================" | ||
| echo "" | ||
| echo " Go to: Settings → Secrets and variables → Actions → New repository secret" | ||
| echo "" | ||
| echo " Add these four secrets:" | ||
| echo "" | ||
| echo " 1. ANDROID_KEYSTORE_BASE64" | ||
| echo " → Value: contents of the 'keystore-base64.txt' file from the" | ||
| echo " 'osu-signing-keystore-base64' artifact (download it above)" | ||
| echo "" | ||
| echo " 2. ANDROID_SIGNING_KEY_ALIAS" | ||
| echo " → Value: ${{ inputs.key_alias }}" | ||
| echo "" | ||
| echo " 3. ANDROID_SIGNING_KEY_PASSWORD" | ||
| echo " → Value: the password you entered when triggering this workflow" | ||
| echo "" | ||
| echo " 4. ANDROID_SIGNING_STORE_PASSWORD" | ||
| echo " → Value: the password you entered when triggering this workflow" | ||
| echo "" | ||
| echo " IMPORTANT:" | ||
| echo " • Keep a backup of the keystore file! If you lose it, you will" | ||
| echo " never be able to update your installed APK — you would have to" | ||
| echo " uninstall and reinstall (losing all local data)." | ||
| echo " • After saving the secrets, DELETE this workflow run to remove" | ||
| echo " the keystore artifact (Settings → Actions → this run → Delete)." | ||
| echo "" | ||
| echo " After saving the secrets, run the 'Build Android APK' workflow." |
There was a problem hiding this comment.
This workflow uploads the signing keystore and its base64 encoding as artifacts. That makes the private signing key retrievable by anyone who can access workflow artifacts, which defeats the purpose of APK signing and enables malicious update signing if leaked. Prefer generating/storing the keystore outside GitHub Actions and adding it directly to repository secrets, or ensure this workflow only runs in a tightly restricted/private repo where artifact access is controlled.
| - name: Encode keystore as base64 | |
| id: encode | |
| run: | | |
| B64=$(base64 -w 0 osu-release.keystore) | |
| echo "$B64" > keystore-base64.txt | |
| echo "encoded=true" >> "$GITHUB_OUTPUT" | |
| - name: Upload keystore artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: osu-signing-keystore | |
| path: osu-release.keystore | |
| retention-days: 1 | |
| - name: Upload base64 artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: osu-signing-keystore-base64 | |
| path: keystore-base64.txt | |
| retention-days: 1 | |
| - name: Output setup instructions | |
| run: | | |
| echo "" | |
| echo "===================================================================" | |
| echo " KEYSTORE GENERATED — SAVE THESE SECRETS NOW" | |
| echo "===================================================================" | |
| echo "" | |
| echo " Go to: Settings → Secrets and variables → Actions → New repository secret" | |
| echo "" | |
| echo " Add these four secrets:" | |
| echo "" | |
| echo " 1. ANDROID_KEYSTORE_BASE64" | |
| echo " → Value: contents of the 'keystore-base64.txt' file from the" | |
| echo " 'osu-signing-keystore-base64' artifact (download it above)" | |
| echo "" | |
| echo " 2. ANDROID_SIGNING_KEY_ALIAS" | |
| echo " → Value: ${{ inputs.key_alias }}" | |
| echo "" | |
| echo " 3. ANDROID_SIGNING_KEY_PASSWORD" | |
| echo " → Value: the password you entered when triggering this workflow" | |
| echo "" | |
| echo " 4. ANDROID_SIGNING_STORE_PASSWORD" | |
| echo " → Value: the password you entered when triggering this workflow" | |
| echo "" | |
| echo " IMPORTANT:" | |
| echo " • Keep a backup of the keystore file! If you lose it, you will" | |
| echo " never be able to update your installed APK — you would have to" | |
| echo " uninstall and reinstall (losing all local data)." | |
| echo " • After saving the secrets, DELETE this workflow run to remove" | |
| echo " the keystore artifact (Settings → Actions → this run → Delete)." | |
| echo "" | |
| echo " After saving the secrets, run the 'Build Android APK' workflow." | |
| - name: Remove generated keystore from runner | |
| if: always() | |
| run: | | |
| rm -f osu-release.keystore keystore-base64.txt | |
| - name: Output setup instructions | |
| run: | | |
| echo "" | |
| echo "===================================================================" | |
| echo " KEYSTORE GENERATED ON RUNNER — NOT EXPORTED" | |
| echo "===================================================================" | |
| echo "" | |
| echo " This workflow no longer uploads the signing keystore or a base64" | |
| echo " copy as artifacts, because doing so would expose the private" | |
| echo " signing key to anyone who can access workflow artifacts." | |
| echo "" | |
| echo " To set up Android signing safely:" | |
| echo "" | |
| echo " 1. Generate the release keystore locally or in another controlled" | |
| echo " environment outside GitHub Actions." | |
| echo " 2. Base64-encode it locally." | |
| echo " 3. Add these repository secrets manually:" | |
| echo "" | |
| echo " • ANDROID_KEYSTORE_BASE64" | |
| echo " • ANDROID_SIGNING_KEY_ALIAS" | |
| echo " • ANDROID_SIGNING_KEY_PASSWORD" | |
| echo " • ANDROID_SIGNING_STORE_PASSWORD" | |
| echo "" | |
| echo " The key alias configured for this run was: ${{ inputs.key_alias }}" | |
| echo "" | |
| echo " After the secrets are configured, run the 'Build Android APK'" | |
| echo " workflow." |
| int num_cores = sysconf(_SC_NPROCESSORS_CONF); | ||
| bigMask = 0; | ||
| if (num_cores > 1) { | ||
| for (int i = num_cores / 2; i < std::min(num_cores, 32); ++i) | ||
| bigMask |= (1 << i); |
There was a problem hiding this comment.
The fallback big-core mask computation uses (1 << i) with int (and i can reach 31). Left-shifting into the sign bit is undefined behaviour in C++ and can produce incorrect masks on some builds/optimisations. Use an unsigned type for the bit operations (e.g. uint32_t/1u << i) and store/convert back to int if required by APIs.
Ephemeral debug keystore generated on every CI run produced different signing certificates, causing
INSTALL_FAILED_UPDATE_INCOMPATIBLEon APK update. Additionally, the workflow required manual multi-step keystore setup, had no versioning, no GitHub Releases, and the README was upstream boilerplate with no documentation of fork differences.Signing & installation fix
ANDROID_KEYSTORE_BASE64secret exists → use itSAVE-THESE-SECRETS.txtartifact containing all 4 secret values ready to paste into repo settingsapksignerVersioning & GitHub Releases
workflow_dispatchnow accepts an optionalversioninput (e.g.1.0.0)YYYY.MMDD.run_numberosu-lazer-VERSION.apksoftprops/action-gh-releasewithmake_latest: true)Runtime crash hardening
oboe_bridge.cpp: Eagerly compute big-core CPU mask instart()— the audio callback was hittingfopen("/sys/...")on a real-time threadOsuGameAndroid.cs: GuardSelectHighestRefreshRateSchedule lambda against stale display referencesREADME rewrite
Comprehensive documentation of all differences from upstream ppy/osu across three repos:
BassAudioMixer.Handlemade public, LINQ/lock hot-path optimisationsVK_KHR_android_surface, OpenGL ES/EGL,System.Threading.Lockmigration, UTF-8 string literals, fence early-out, GLES stencil fixes