From 67aa54f18108eecfcc101476ab0e4a511c545bf8 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Wed, 1 Jul 2026 00:06:53 -0500 Subject: [PATCH 1/2] fix(android): brand-green app icon + tag-derived APK versioning Two Android packaging fixes plus install docs. App icon: the adaptive-icon background was #0b0e12 (near-black), so the dark #111 foreground mark rendered as "black on grey". Set it to the SharpMUSH brand green #00f5b7 so the mark reads cleanly on a proper branded background. Release versioning: the APK hardcoded ApplicationDisplayVersion=1.0 / ApplicationVersion=1, and the release workflow never overrode them. Every build shipped versionName 1.0 / versionCode 1 regardless of the release tag, so Obtainium showed "1.0" against a "v0.2" release (and never offered updates), and Android refused in-place updates because the versionCode never changed. The release workflow now derives both from the tag: versionName = tag minus v/pre- release suffix; versionCode = major*1e6 + minor*1e4 + patch*100 (monotonic). README: document installing via Obtainium (source URL + deep link), the single universal APK, tag-derived versioning, and the pre-release toggle. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- .github/workflows/release-apk.yml | 33 ++++++++++++++++++++++ README.md | 22 +++++++++++++++ src/SharpClient.App/SharpClient.App.csproj | 10 ++++--- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-apk.yml b/.github/workflows/release-apk.yml index cdaefc9..55b58a7 100644 --- a/.github/workflows/release-apk.yml +++ b/.github/workflows/release-apk.yml @@ -68,6 +68,35 @@ jobs: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > "$KEYSTORE_PATH" echo "Decoded keystore to $KEYSTORE_PATH" + # Derive the Android versionName + versionCode from the release tag so the + # installed APK reports the same version the GitHub Release advertises. + # Without this, every build ships the csproj defaults (versionName 1.0, + # versionCode 1): Obtainium then shows "1.0" against a "v0.2" release, and + # Android refuses in-place updates because the versionCode never changes. + # versionName = tag with any leading 'v' and pre-release suffix stripped + # versionCode = major*1000000 + minor*10000 + patch*100 (monotonic with semver) + # Manual (workflow_dispatch) runs have no tag: fall back to 0.0.0 + run_number. + - name: Compute version from release tag + id: ver + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # v0.2.1 -> 0.2.1 + VERSION="${VERSION%%-*}" # 0.2.1-rc1 -> 0.2.1 + else + VERSION="0.0.0" + fi + IFS='.' read -r MAJ MIN PAT <<< "$VERSION" + MAJ=${MAJ:-0}; MIN=${MIN:-0}; PAT=${PAT:-0} + if [ "${{ github.event_name }}" = "release" ]; then + CODE=$(( 10#$MAJ * 1000000 + 10#$MIN * 10000 + 10#$PAT * 100 )) + else + CODE=${{ github.run_number }} + fi + echo "display=$VERSION" >> "$GITHUB_OUTPUT" + echo "code=$CODE" >> "$GITHUB_OUTPUT" + echo "Resolved versionName=$VERSION versionCode=$CODE" + # Publish a SIGNED .apk (sideload). AndroidKeyStore=true + the signing # properties make dotnet sign with our release keystore instead of the # auto-generated debug key. AndroidPackageFormat=apk is the default but is @@ -80,6 +109,8 @@ jobs: -p:JavaSdkDirectory="$JAVA_HOME" -p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" -p:AcceptAndroidSdkLicenses=True + -p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }} + -p:ApplicationVersion=${{ steps.ver.outputs.code }} -p:AndroidPackageFormat=apk -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="$KEYSTORE_PATH" @@ -97,6 +128,8 @@ jobs: -p:JavaSdkDirectory="$JAVA_HOME" -p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" -p:AcceptAndroidSdkLicenses=True + -p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }} + -p:ApplicationVersion=${{ steps.ver.outputs.code }} -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="$KEYSTORE_PATH" diff --git a/README.md b/README.md index 3518e01..eb925b0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,28 @@ over telnet, using [TelnetNegotiationCore](https://www.nuget.org/packages/Telnet Android-primary. See [`docs/superpowers/specs`](docs/superpowers/specs) for the design spec and visual-design brief. +## Installing on Android + +Signed APKs are attached to every [GitHub Release](https://github.com/SharpMUSH/SharpClient/releases). +You can sideload the `.apk` directly, but the easiest way to install **and stay +updated** is [Obtainium](https://github.com/ImranR98/Obtainium), which tracks this +repo's releases for you: + +1. Install Obtainium. +2. Add an app with this source URL: + ``` + https://github.com/SharpMUSH/SharpClient + ``` + or use the deep link [obtainium://add/https://github.com/SharpMUSH/SharpClient](obtainium://add/https://github.com/SharpMUSH/SharpClient). +3. Obtainium picks up the universal signed APK from each release and notifies you + when a new version ships. + +Each release ships a single universal APK, so no ABI/architecture filter is +needed. The APK's `versionName`/`versionCode` are derived from the release tag +(e.g. `v0.2` → `0.2`), so Obtainium and Android both see the correct version and +can update in place. If a release is marked **pre-release** on GitHub, enable +"include prereleases" for the app in Obtainium or it will be skipped. + ## Solution layout | Project | Kind | Purpose | diff --git a/src/SharpClient.App/SharpClient.App.csproj b/src/SharpClient.App/SharpClient.App.csproj index a4dc0dd..1f3863f 100644 --- a/src/SharpClient.App/SharpClient.App.csproj +++ b/src/SharpClient.App/SharpClient.App.csproj @@ -69,11 +69,13 @@ - + From 1ceb9d4765119a30fa426740aaf03ab263d6affd Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Wed, 1 Jul 2026 00:15:03 -0500 Subject: [PATCH 2/2] fix(android): harden release version parsing + README obtainium link Addresses review feedback on the versioning workflow and README: - Strip SemVer build metadata (+...) from the tag before parsing, so tags like v1.2.3+4 no longer corrupt the patch component. - Widen the versionCode formula to major*1e8 + minor*1e4 + patch so larger minor/patch values keep monotonic ordering (previously 0.1.123 produced a code above 0.2.0, which Android would treat as a downgrade). Fail fast when a tag isn't numeric semver or exceeds the supported range (major<=20, minor/patch<=9999, staying under Android's 2.1e9 versionCode cap). - Non-release (workflow_dispatch) builds now use a fixed 0.0.0-dev / versionCode 1 instead of github.run_number, so a dispatched build can never overtake a real release's versionCode and block its in-place update. - README: present the obtainium:// deep link as copyable code, since GitHub's markdown sanitizer strips non-http(s) schemes and won't render it clickable. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- .github/workflows/release-apk.yml | 37 ++++++++++++++++++++----------- README.md | 6 ++++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release-apk.yml b/.github/workflows/release-apk.yml index 55b58a7..49adacd 100644 --- a/.github/workflows/release-apk.yml +++ b/.github/workflows/release-apk.yml @@ -73,26 +73,37 @@ jobs: # Without this, every build ships the csproj defaults (versionName 1.0, # versionCode 1): Obtainium then shows "1.0" against a "v0.2" release, and # Android refuses in-place updates because the versionCode never changes. - # versionName = tag with any leading 'v' and pre-release suffix stripped - # versionCode = major*1000000 + minor*10000 + patch*100 (monotonic with semver) - # Manual (workflow_dispatch) runs have no tag: fall back to 0.0.0 + run_number. + # versionName = tag with leading 'v', pre-release (-...) and build (+...) stripped + # versionCode = major*100000000 + minor*10000 + patch (monotonic with semver; + # room for minor/patch < 10000, major <= 20 to stay under Android's + # 2,100,000,000 versionCode cap). Fails fast on out-of-range/bad tags. + # Manual (workflow_dispatch) runs only upload to the run (never attached to a + # release), so they use a fixed 0.0.0-dev / versionCode 1 that can never overtake a + # real release and block its in-place update. - name: Compute version from release tag id: ver run: | - if [ "${{ github.event_name }}" = "release" ]; then - VERSION="${{ github.event.release.tag_name }}" - VERSION="${VERSION#v}" # v0.2.1 -> 0.2.1 - VERSION="${VERSION%%-*}" # 0.2.1-rc1 -> 0.2.1 - else - VERSION="0.0.0" + if [ "${{ github.event_name }}" != "release" ]; then + echo "display=0.0.0-dev" >> "$GITHUB_OUTPUT" + echo "code=1" >> "$GITHUB_OUTPUT" + echo "Non-release build: versionName=0.0.0-dev versionCode=1" + exit 0 fi + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # v0.2.1 -> 0.2.1 + VERSION="${VERSION%%+*}" # 0.2.1+build -> 0.2.1 (strip build metadata) + VERSION="${VERSION%%-*}" # 0.2.1-rc1 -> 0.2.1 (strip pre-release) IFS='.' read -r MAJ MIN PAT <<< "$VERSION" MAJ=${MAJ:-0}; MIN=${MIN:-0}; PAT=${PAT:-0} - if [ "${{ github.event_name }}" = "release" ]; then - CODE=$(( 10#$MAJ * 1000000 + 10#$MIN * 10000 + 10#$PAT * 100 )) - else - CODE=${{ github.run_number }} + if ! [[ "$MAJ" =~ ^[0-9]+$ && "$MIN" =~ ^[0-9]+$ && "$PAT" =~ ^[0-9]+$ ]]; then + echo "::error::Release tag '${{ github.event.release.tag_name }}' is not a numeric semver (parsed major='$MAJ' minor='$MIN' patch='$PAT')." >&2 + exit 1 + fi + if [ "$MAJ" -gt 20 ] || [ "$MIN" -gt 9999 ] || [ "$PAT" -gt 9999 ]; then + echo "::error::Version $VERSION out of versionCode range (require major<=20, minor/patch<=9999)." >&2 + exit 1 fi + CODE=$(( 10#$MAJ * 100000000 + 10#$MIN * 10000 + 10#$PAT )) echo "display=$VERSION" >> "$GITHUB_OUTPUT" echo "code=$CODE" >> "$GITHUB_OUTPUT" echo "Resolved versionName=$VERSION versionCode=$CODE" diff --git a/README.md b/README.md index eb925b0..e168c04 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ repo's releases for you: ``` https://github.com/SharpMUSH/SharpClient ``` - or use the deep link [obtainium://add/https://github.com/SharpMUSH/SharpClient](obtainium://add/https://github.com/SharpMUSH/SharpClient). + or, from your phone, paste this Obtainium deep link (GitHub can't render it as + a clickable link, so copy it as-is): + ``` + obtainium://add/https://github.com/SharpMUSH/SharpClient + ``` 3. Obtainium picks up the universal signed APK from each release and notifies you when a new version ships.