Skip to content

Fix INSTALL_FAILED_UPDATE_INCOMPATIBLE, add auto-signing, versioning, releases, and fork documentation - #205

Merged
winnerspiros merged 7 commits into
masterfrom
copilot/review-installation-failure-issues
Apr 19, 2026
Merged

Fix INSTALL_FAILED_UPDATE_INCOMPATIBLE, add auto-signing, versioning, releases, and fork documentation#205
winnerspiros merged 7 commits into
masterfrom
copilot/review-installation-failure-issues

Conversation

Copilot AI commented Apr 19, 2026

Copy link
Copy Markdown

Ephemeral debug keystore generated on every CI run produced different signing certificates, causing INSTALL_FAILED_UPDATE_INCOMPATIBLE on 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

  • Removed the "generate fresh debug keystore per build" fallback that was the root cause
  • When ANDROID_KEYSTORE_BASE64 secret exists → use it
  • When missing → auto-generate a keystore, sign with it, upload SAVE-THESE-SECRETS.txt artifact containing all 4 secret values ready to paste into repo settings
  • Added APK signature verification step via apksigner

Versioning & GitHub Releases

  • workflow_dispatch now accepts an optional version input (e.g. 1.0.0)
  • Fallback chain: manual input → git tag → auto-generated YYYY.MMDD.run_number
  • APK renamed to osu-lazer-VERSION.apk
  • Always creates a GitHub Release with the APK attached (softprops/action-gh-release with make_latest: true)

Runtime crash hardening

  • oboe_bridge.cpp: Eagerly compute big-core CPU mask in start() — the audio callback was hitting fopen("/sys/...") on a real-time thread
  • OsuGameAndroid.cs: Guard SelectHighestRefreshRate Schedule lambda against stale display references

README rewrite

Comprehensive documentation of all differences from upstream ppy/osu across three repos:

  • osu: Oboe audio bridge, S Pen/stylus input, CPU affinity pinning, Samsung DeX, Vulkan probing, Samsung Game Launcher integration, performance mode, CI/CD
  • osu-framework: .NET 10 upgrade, BASS buffer tuning (100ms → 25ms), Vulkan as primary Android renderer, BassAudioMixer.Handle made public, LINQ/lock hot-path optimisations
  • veldrid: Android Vulkan surface creation via VK_KHR_android_surface, OpenGL ES/EGL, System.Threading.Lock migration, UTF-8 string literals, fence early-out, GLES stencil fixes

Copilot AI and others added 7 commits April 19, 2026 07:52
…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>
@gitar-bot

gitar-bot Bot commented Apr 19, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@winnerspiros
winnerspiros marked this pull request as ready for review April 19, 2026 08:27
Copilot AI review requested due to automatic review settings April 19, 2026 08:27
@winnerspiros
winnerspiros merged commit b4b705d into master Apr 19, 2026
3 of 15 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 85 to +92
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"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +166
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]).

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +264
# 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 "=================================================================="

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +110
- 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."

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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."

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +298
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);

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants