Skip to content

Commit fb2e136

Browse files
fix: nightly update checker never detected an update, at all
Root cause: when I consolidated the Android-only release-android.yml into release-latest.yml (covering all 4 platforms), I rewrote every job's release-body text and dropped the explicit "commit: <sha>" line the original had — the new body just said "Rebuilt from `<sha>`,". That line is exactly what UpdateChecker.extractCommitSha's regex requires ("commit:" literal + hex chars); without it, every fetched release parsed with commitSha = null, and isUpdateAvailable short-circuits to false whenever the fetched commitSha is null. So the checker silently always reported "no update available," with no error anywhere — it looked completely broken from the user's side, confirmed live against the actual GitHub Release body (`gh release view latest`). Two fixes, layered so this class of regression can't silently recur: 1. Restored the explicit "commit: <sha>" line in all 4 platform jobs' release bodies in release-latest.yml. 2. Made extractCommitSha resilient on its own: falls back to matching a bare 40-hex-char sha anywhere in the body (unambiguous enough to be safe unlabeled, unlike a short sha) if no "commit:"-labeled line is found, so a future body-text wording tweak can't quietly break parsing again the same way. Also: versionCode/versionName were hardcoded to 1/"1.0" forever, so every nightly build was indistinguishable from any other to Android itself (and to a user checking Settings > App Info) — addressing the "needs to be based on a build/commit number" ask directly. versionCode now derives from `git rev-list --count HEAD` (monotonically increasing across this repo's history by construction); versionName includes it plus the short commit sha (e.g. "1.0.138-g188c156"). Had to also set `fetch-depth: 0` on build-android's checkout step — actions/checkout@v4 defaults to a shallow clone, under which `git rev-list --count HEAD` only ever sees 1 commit and versionCode would've silently stayed 1 forever in CI regardless of this fix. Verified locally: a real build now produces versionCode='138' versionName='1.0.138-g188c156' via aapt2 dump badging, matching this checkout's actual `git log --oneline | wc -l` and HEAD sha. 3 new tests covering the bare-sha fallback and the labeled-line-still-wins precedence; 74 tests total, 0 failures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 8d6ea57 commit fb2e136

4 files changed

Lines changed: 75 additions & 4 deletions

File tree

.github/workflows/release-latest.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ jobs:
9696
contents: write
9797
steps:
9898
- uses: actions/checkout@v4
99+
with:
100+
# Full history, not the default shallow (fetch-depth: 1) clone —
101+
# mobile/app/build.gradle.kts derives versionCode from
102+
# `git rev-list --count HEAD`, which only sees 1 commit (and so
103+
# always resolves to versionCode 1, forever) on a shallow clone.
104+
fetch-depth: 0
99105

100106
- name: Set up JDK
101107
uses: actions/setup-java@v4
@@ -118,6 +124,8 @@ jobs:
118124
Android build is DEBUG-SIGNED (sideload only, not from the Play
119125
Store) — see docs/PACKAGING.md. Rebuilt from `${{ github.sha }}`,
120126
replaced on every push to master.
127+
128+
commit: ${{ github.sha }}
121129
prerelease: true
122130
files: dist/metanoia-android-debug.apk
123131

@@ -163,6 +171,8 @@ jobs:
163171
macOS build (unsigned/ad-hoc-signed — see docs/PACKAGING.md for
164172
the Gatekeeper caveat). Unzip and run Metanoia.app. Rebuilt from
165173
`${{ github.sha }}`, replaced on every push to master.
174+
175+
commit: ${{ github.sha }}
166176
prerelease: true
167177
files: dist/Metanoia-macos-*.tar.gz
168178

@@ -210,6 +220,8 @@ jobs:
210220
Linux build: universal tarball (with install.sh) + .deb for
211221
Debian/Ubuntu — see docs/PACKAGING.md. Rebuilt from
212222
`${{ github.sha }}`, replaced on every push to master.
223+
224+
commit: ${{ github.sha }}
213225
prerelease: true
214226
files: |
215227
dist/Metanoia-linux-*.tar.gz
@@ -277,5 +289,7 @@ jobs:
277289
body: |
278290
Portable Windows build — unzip and run metanoia.exe. Rebuilt
279291
from `${{ github.sha }}`, replaced on every push to master.
292+
293+
commit: ${{ github.sha }}
280294
prerelease: true
281295
files: dist/Metanoia-latest.zip

mobile/app/build.gradle.kts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ android {
3333
applicationId = "com.bytecats.metanoia"
3434
minSdk = 28
3535
targetSdk = 35
36-
versionCode = 1
37-
versionName = "1.0"
36+
// Was hardcoded to 1/"1.0" forever, so every nightly build looked
37+
// identical to Android and to a user checking Settings > App Info —
38+
// no way to tell which build is actually installed short of BuildConfig.
39+
// versionCode = total commit count: monotonically increasing across
40+
// this repo's history by construction, exactly what Android wants.
41+
versionCode = gitCommitCount()
42+
versionName = "1.0.${gitCommitCount()}-g${gitCommitSha().take(7)}"
3843
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3944
buildConfigField("String", "GIT_COMMIT_SHA", "\"${gitCommitSha()}\"")
4045
}
@@ -79,6 +84,16 @@ fun gitCommitSha(): String = try {
7984
if (output.matches(Regex("^[0-9a-f]{40}$"))) output else "unknown"
8085
} catch (e: Exception) { "unknown" }
8186

87+
fun gitCommitCount(): Int = try {
88+
val process = ProcessBuilder("git", "rev-list", "--count", "HEAD")
89+
.directory(rootDir)
90+
.redirectErrorStream(true)
91+
.start()
92+
val output = process.inputStream.bufferedReader().readText().trim()
93+
process.waitFor()
94+
output.toIntOrNull() ?: 1
95+
} catch (e: Exception) { 1 }
96+
8297
dependencies {
8398
implementation("androidx.core:core-ktx:1.15.0")
8499
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")

mobile/app/src/main/java/com/bytecats/metanoia/update/UpdateChecker.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ object UpdateChecker {
3737
// tolerating surrounding whitespace.
3838
private val COMMIT_SHA_REGEX = Regex("""(?i)commit:\s*([0-9a-f]{7,40})\s*""")
3939

40+
// Fallback for when the body doesn't have an explicit "commit:" label —
41+
// this happened for real: a release-workflow body-text rewrite dropped
42+
// the labeled line while still mentioning the sha in prose (e.g.
43+
// "Rebuilt from `<sha>`"), which silently made every update check think
44+
// no commit sha was ever available (isUpdateAvailable degrades to
45+
// "false" when commitSha is null) — the whole checker looked broken
46+
// with no error anywhere. A bare 40-hex-char sha is unambiguous enough
47+
// to match unlabeled (unlike a short sha, which is too easily confused
48+
// with an unrelated hex-looking token), so this is a safe last resort.
49+
private val BARE_FULL_SHA_REGEX = Regex("""(?i)\b([0-9a-f]{40})\b""")
50+
4051
/**
4152
* Parses a GitHub Releases API JSON response body. Returns null on any
4253
* malformed input (missing/blank tag_name, invalid JSON, unexpected
@@ -79,11 +90,14 @@ object UpdateChecker {
7990
}
8091

8192
/**
82-
* Extracts the 7-40 char hex commit sha out of a "commit: <sha>" line in
83-
* the release body, or null if no such line is present.
93+
* Extracts a commit sha from the release body: prefers an explicit
94+
* "commit: <sha>" line, falls back to a bare 40-char hex sha anywhere in
95+
* the text (see BARE_FULL_SHA_REGEX for why), or null if neither is
96+
* present.
8497
*/
8598
fun extractCommitSha(body: String): String? =
8699
COMMIT_SHA_REGEX.find(body)?.groupValues?.get(1)
100+
?: BARE_FULL_SHA_REGEX.find(body)?.groupValues?.get(1)
87101

88102
/**
89103
* Pure comparison: is `fetched` a genuinely different build than the one

mobile/app/src/test/java/com/bytecats/metanoia/UpdateCheckerTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ class UpdateCheckerTest {
147147
assertNull(UpdateChecker.extractCommitSha("nothing relevant"))
148148
}
149149

150+
@Test
151+
fun extractCommitShaFallsBackToBareFullShaWhenNoCommitLabel() {
152+
// Regression test: release-latest.yml's body text was rewritten to
153+
// "Rebuilt from `<sha>`, replaced on every push to master." with no
154+
// "commit:" label at all, which silently made every update check
155+
// think no update was ever available (see isUpdateAvailable) — the
156+
// in-app checker looked completely broken with no visible error.
157+
val sha = UpdateChecker.extractCommitSha(
158+
"Android build is DEBUG-SIGNED. Rebuilt from `$fullShaOne`, replaced on every push to master."
159+
)
160+
assertEquals(fullShaOne, sha)
161+
}
162+
163+
@Test
164+
fun extractCommitShaPrefersLabeledLineOverBareShaWhenBothPresent() {
165+
val otherSha = "9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f"
166+
val sha = UpdateChecker.extractCommitSha("mentions `$otherSha` in passing\ncommit: $fullShaOne")
167+
assertEquals(fullShaOne, sha)
168+
}
169+
170+
@Test
171+
fun extractCommitShaDoesNotMatchBareShortShaWithoutLabel() {
172+
// Short (non-40-char) shas are too easily confused with unrelated
173+
// hex-looking tokens to safely match unlabeled — only the labeled
174+
// "commit:" form (tested elsewhere) accepts short shas.
175+
assertNull(UpdateChecker.extractCommitSha("built from a1b2c3d today"))
176+
}
177+
150178
// -------------------------------------------------------------------
151179
// isUpdateAvailable
152180
// -------------------------------------------------------------------

0 commit comments

Comments
 (0)