Version builds from the CI run number

24a90d7 · gmorell · 2026-11-05 16:30

6 files +68 -3
Message
{commit_body(@commit)}

Files changed

modified .forgejo/workflows/android.yml
+12 −0
@@ -81,9 +81,21 @@ jobs:
81 81
82 82 - name: Build and sign the APK
83 83 if: steps.keystore.outputs.skip != 'true'
84 + env:
85 + RUN_NUMBER: ${{ github.run_number }}
84 86 run: |
87 + set -euo pipefail
88 +
89 + # One stamp for the whole run, computed here rather than
90 + # inside Gradle, so every artifact a run produces agrees on
91 + # its version even if the build is retried a step at a time.
92 + stamp=$(date -u +%s)
93 + echo "version: 0.1.0-dev-$stamp (versionCode $RUN_NUMBER)"
94 +
85 95 docker buildx build \
86 96 --target android-export \
97 + --build-arg GITHUB_RUN_NUMBER="$RUN_NUMBER" \
98 + --build-arg SOURCE_DATE_EPOCH="$stamp" \
87 99 --secret id=keystore,src=./release.jks \
88 100 --secret id=keystore_password,src=./keystore.pass \
89 101 --output type=local,dest=./apk \
modified Dockerfile
+18 −1
@@ -77,9 +77,26 @@ RUN ./gradlew --no-daemon :app:dependencies --configuration releaseRuntimeClassp
77 77 FROM deps AS build
78 78
79 79 ARG BUILD_TOOLS=34.0.0
80 +
81 +# A container build inherits nothing from the runner's environment, so
82 +# the version has to be handed in explicitly. Without these, Gradle's
83 +# getenv lookups come back empty inside here and every run builds
84 +# versionCode 1 — the same filename with different bytes, which the
85 +# registry refuses with a 409.
86 +#
87 +# Declared in this stage rather than at the top so they don't invalidate
88 +# the SDK and dependency layers, which is most of the build time.
89 +ARG GITHUB_RUN_NUMBER
90 +ARG SOURCE_DATE_EPOCH
91 +ARG RELEASE_VERSION
92 +ENV GITHUB_RUN_NUMBER=${GITHUB_RUN_NUMBER} \
93 + SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
94 + RELEASE_VERSION=${RELEASE_VERSION}
95 +
80 96 COPY app ./app
81 97
82 −RUN ./gradlew --no-daemon :app:assembleRelease
98 +RUN echo "building versionCode=${GITHUB_RUN_NUMBER:-1}" \
99 + && ./gradlew --no-daemon :app:assembleRelease
83 100
84 101 # Sign as a separate step from Gradle so the keystore never reaches a
85 102 # build script, a Gradle cache, or an image layer. BuildKit secrets are
modified app/build.gradle.kts
+38 −2
@@ -4,6 +4,27 @@ plugins {
4 4 alias(libs.plugins.kotlin.compose)
5 5 }
6 6
7 +// Bumped by hand; everything after it is generated.
8 +val baseVersion = "0.1.0"
9 +
10 +// Seconds, and taken from the environment when it's there. CI sets it
11 +// once per run so every artifact from that run agrees; a local build
12 +// computes it, which is fine because local builds are never published.
13 +//
14 +// SOURCE_DATE_EPOCH first because it's the cross-toolchain convention
15 +// for exactly this, and honouring it means a reproducible-build harness
16 +// that already sets it gets a reproducible versionName for free.
17 +val buildTimestamp: Long =
18 + (System.getenv("SOURCE_DATE_EPOCH") ?: System.getenv("BUILD_TIMESTAMP"))
19 + ?.toLongOrNull()
20 + ?: (System.currentTimeMillis() / 1000)
21 +
22 +// A real release overrides the whole string; until there is one, every
23 +// build says so in its name rather than claiming to be 0.1.0.
24 +val appVersionName: String =
25 + System.getenv("RELEASE_VERSION")?.takeIf { it.isNotBlank() }
26 + ?: "$baseVersion-dev-$buildTimestamp"
27 +
7 28 android {
8 29 namespace = "org.neiam.mediabubbles"
9 30 compileSdk = 34
@@ -12,8 +33,23 @@ android {
12 33 applicationId = "org.neiam.mediabubbles"
13 34 minSdk = 26
14 35 targetSdk = 34
15 − versionCode = 1
16 − versionName = "0.1.0"
36 + // Android refuses an update whose versionCode isn't higher than
37 + // what's installed, and F-Droid orders versions by it — so a
38 + // hardcoded 1 means no phone ever sees a second release. It also
39 + // means every CI build publishes the same filename with
40 + // different bytes, which the registry correctly refuses with a
41 + // 409.
42 + //
43 + // The CI run number is monotonic and needs no state kept
44 + // anywhere. Local builds stay at 1, which is fine: they are
45 + // never published.
46 + versionCode = (System.getenv("GITHUB_RUN_NUMBER") ?: "").toIntOrNull() ?: 1
47 +
48 + // Not the commit: versionCode is the CI run number, and a run
49 + // knows its own commit, so the sha is one lookup away and the
50 + // timestamp orders builds at a glance in a list that versionCode
51 + // alone renders as bare integers.
52 + versionName = appVersionName
17 53 }
18 54
19 55 buildTypes {

Parents: eddae77