Give the Android app a versionCode that moves

74e1fb0 · Gabriel Morell · 2026-09-12 20:04

12 files +51 -3
Message
{commit_body(@commit)}

Files changed

modified android/app/build.gradle.kts
+19 −2
@@ -5,6 +5,10 @@ plugins {
5 5 alias(libs.plugins.kotlin.serialization)
6 6 }
7 7
8 +// Bumped by hand for a real release; the build metadata after it comes
9 +// from CI.
10 +val baseVersion = "0.1.0"
11 +
8 12 android {
9 13 namespace = "org.neiam.gitgud.app"
10 14 compileSdk = 34
@@ -13,8 +17,21 @@ android {
13 17 applicationId = "org.neiam.gitgud.app"
14 18 minSdk = 26
15 19 targetSdk = 34
16 − versionCode = 1
17 − versionName = "0.1.0"
20 + // Android refuses an update whose versionCode isn't higher than
21 + // what's installed, and F-Droid orders versions by it — so a
22 + // hardcoded 1 means no phone ever sees a second release. It also
23 + // means every CI build publishes the same filename with
24 + // different bytes, which the registry correctly refuses with a
25 + // 409.
26 + //
27 + // The CI run number is monotonic and needs no state kept
28 + // anywhere. Local builds stay at 1, which is fine: they are
29 + // never published.
30 + versionCode = (System.getenv("GITHUB_RUN_NUMBER") ?: "").toIntOrNull() ?: 1
31 +
32 + // The commit is what you want when someone reports a bug
33 + // against a build. Local builds just say "dev".
34 + versionName = "$baseVersion+" + ((System.getenv("GITHUB_SHA") ?: "dev").take(7))
18 35 }
19 36
20 37 buildTypes {
modified lib/git_gud/packages/formats/fdroid.ex
+11 −1
@@ -461,8 +461,18 @@ defmodule GitGud.Packages.Formats.Fdroid do
461 461 "filename" => Apk.filename(info)
462 462 })}
463 463
464 + # The generic "a file with that name is already published"
465 + # doesn't point anywhere useful here, and this has exactly one
466 + # cause worth naming: a versionCode that didn't move. Android
467 + # refuses an update that doesn't increase it and F-Droid orders
468 + # by it, so a repeated one is a bug in the build rather than a
469 + # publishing accident.
464 470 {:error, :file_exists} ->
465 − {:error, :file_exists}
471 + {:error,
472 + {409,
473 + "#{Apk.filename(info)} is already published with different content — " <>
474 + "versionCode #{info.version_code} has been used before. Bump it: Android " <>
475 + "won't install an update whose versionCode hasn't increased."}}
466 476
467 477 {:error, reason} ->
468 478 {:error, {400, inspect(reason)}}
modified test/git_gud/packages/fdroid_format_test.exs
+21 −0
@@ -116,6 +116,27 @@ defmodule GitGud.Packages.Formats.FdroidTest do
116 116 assert file.metadata["nativecode"] == ["arm64-v8a"]
117 117 end
118 118
119 + test "republishing a versionCode says which one, and why", %{repo: repo} do
120 + apk = build_apk()
121 + {:ok, _, _} = publish(repo, apk)
122 +
123 + # Same versionCode, different bytes — which is what a CI build
124 + # with a hardcoded versionCode produces on its second run.
125 + different = build_apk([{~c"extra.txt", "changes the digest"}])
126 +
127 + assert {:error, {409, message}} = publish(repo, different)
128 + assert message =~ "org.neiam.gitgud.app_1.apk"
129 + assert message =~ "versionCode 1"
130 + assert message =~ "won't install an update"
131 + end
132 +
133 + test "republishing identical bytes is fine", %{repo: repo} do
134 + apk = build_apk()
135 + assert {:ok, _, _} = publish(repo, apk)
136 + # A re-run that produced the same artifact is not a conflict.
137 + assert {:ok, _, _} = publish(repo, apk)
138 + end
139 +
119 140 test "refuses something that isn't an APK", %{repo: repo} do
120 141 assert {:error, {422, _}} = publish(repo, "definitely not a zip")
121 142 end

Parents: 1d76240