Versioning rework, a dev channel, and the miniplayer gap #129
@@ -24,9 +24,10 @@ name: release
|
|||||||
# :latest (not just tags), a main build with no APK would silently strip
|
# :latest (not just tags), a main build with no APK would silently strip
|
||||||
# the in-app update channel off :latest until the next release. So on
|
# the in-app update channel off :latest until the next release. So on
|
||||||
# non-tag builds image-release pulls the MOST RECENT release's signed APK
|
# non-tag builds image-release pulls the MOST RECENT release's signed APK
|
||||||
# and reconstructs its exact versionName (tag + commit-count, the same
|
# and reconstructs its exact versionName from the tagged commit's timestamp
|
||||||
# formula android-release bakes in) for the version sidecar — no rebuild,
|
# (the same derivation android-release bakes in) for the version sidecar —
|
||||||
# just rebundle. Tag builds keep bundling their own freshly-built APK.
|
# no rebuild, just rebundle. Tag builds keep bundling their own
|
||||||
|
# freshly-built APK.
|
||||||
#
|
#
|
||||||
# Android testing (lint + detekt + unit tests, debug APK upload on main)
|
# Android testing (lint + detekt + unit tests, debug APK upload on main)
|
||||||
# lives in android.yml and runs independently on every push.
|
# lives in android.yml and runs independently on every push.
|
||||||
@@ -80,9 +81,12 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# fetch-depth: 0 retrieves full history; default shallow clone
|
# Full history. The version name now reads only the tip commit's
|
||||||
# would return 1 for `git rev-list --count HEAD`, breaking the
|
# timestamp, so a shallow clone would technically serve — but this
|
||||||
# iteration suffix.
|
# job derives a value that ships to devices, and a shallow checkout
|
||||||
|
# changes what git-derived values resolve to WITHOUT failing. The
|
||||||
|
# whole failure class here is a green build carrying a wrong
|
||||||
|
# version, so the cheap guarantee is worth keeping.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Compute release version
|
- name: Compute release version
|
||||||
@@ -91,12 +95,45 @@ jobs:
|
|||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG="${GITHUB_REF#refs/tags/v}"
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
# Two different clocks, deliberately. They answer different
|
||||||
VERSION_NAME="${TAG}.${COMMIT_COUNT}"
|
# questions, and using one for both breaks whichever it fits worse.
|
||||||
|
#
|
||||||
|
# The NAME answers "is this the same code?" — so it derives from
|
||||||
|
# COMMIT time and reads identically on every lane that builds this
|
||||||
|
# source. A dev build and a main build of one commit must report the
|
||||||
|
# same string; build time cannot do that, it prints two numbers for
|
||||||
|
# one thing.
|
||||||
|
COMMIT_TS=$(git log --format=%ct -1 HEAD)
|
||||||
|
VERSION_NAME=$(date -u -d "@${COMMIT_TS}" +%Y.%m.%d.%H%M)
|
||||||
|
|
||||||
|
# The ORDERING KEY answers "may this be installed over that?" — so it
|
||||||
|
# must be monotonic BY CONSTRUCTION. Minutes since 2020-01-01: ~3.5M
|
||||||
|
# today, ~525k/year, against a 2^31 ceiling.
|
||||||
|
#
|
||||||
|
# This replaced `git rev-list --count HEAD`, which was NOT monotonic
|
||||||
|
# and was commented as if it were. A commit count runs ahead on `dev`,
|
||||||
|
# so a dev build outranked the `main` release that superseded it and
|
||||||
|
# Android refused the install as a downgrade — a channel you could
|
||||||
|
# enter and not leave without uninstalling.
|
||||||
|
#
|
||||||
|
# Commit time would be wrong here too, for the mirror-image reason:
|
||||||
|
# rebuild an older commit and it goes DOWN, which on a phone is a
|
||||||
|
# refused install rather than a merely confusing label.
|
||||||
|
VERSION_CODE=$(( ( $(date -u +%s) - 1577836800 ) / 60 ))
|
||||||
|
|
||||||
|
# Assert the emitted shape at the source. A malformed name still
|
||||||
|
# builds, signs and publishes perfectly happily, and only surfaces as
|
||||||
|
# an update nobody is ever offered — which nobody reports, because
|
||||||
|
# "no update available" and "I cannot read this" look identical.
|
||||||
|
if [[ ! "${VERSION_NAME}" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}$ ]]; then
|
||||||
|
echo "::error::version name '${VERSION_NAME}' is not YYYY.MM.DD.HHMM"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "name=${VERSION_NAME}" >> "$GITHUB_OUTPUT"
|
echo "name=${VERSION_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "code=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT"
|
echo "code=${VERSION_CODE}" >> "$GITHUB_OUTPUT"
|
||||||
echo "::notice::APK version: ${VERSION_NAME} (code=${COMMIT_COUNT})"
|
echo "::notice::APK version: ${VERSION_NAME} (code=${VERSION_CODE})"
|
||||||
|
|
||||||
# Checked BEFORE the expensive work, not after it. "Attach APK to gitea
|
# Checked BEFORE the expensive work, not after it. "Attach APK to gitea
|
||||||
# Release" below resolves the release by tag and fails if it is absent —
|
# Release" below resolves the release by tag and fails if it is absent —
|
||||||
@@ -223,8 +260,9 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Full history + tags so non-tag :latest builds can resolve the
|
# Full history + tags so non-tag :latest builds can resolve the
|
||||||
# latest release tag's commit count and reconstruct the bundled
|
# latest release tag's commit and reconstruct the bundled APK's
|
||||||
# APK's exact versionName (see "Bundle latest release APK" below).
|
# exact versionName from its timestamp (see "Bundle latest release
|
||||||
|
# APK" below).
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
|
|
||||||
@@ -309,9 +347,10 @@ jobs:
|
|||||||
# Main pushes don't build an APK, but they DO move :latest — so
|
# Main pushes don't build an APK, but they DO move :latest — so
|
||||||
# without this the in-app update channel would vanish from :latest
|
# without this the in-app update channel would vanish from :latest
|
||||||
# until the next tag. Pull the most-recent release's signed APK and
|
# until the next tag. Pull the most-recent release's signed APK and
|
||||||
# reconstruct its exact versionName (${TAG#v}.$(git rev-list --count
|
# reconstruct its exact versionName from the tagged commit's
|
||||||
# TAG) — identical to android-release's formula) so the version
|
# timestamp — the same derivation android-release uses — so the
|
||||||
# sidecar the server hands clients matches the installed build.
|
# version sidecar the server hands clients matches the installed
|
||||||
|
# build.
|
||||||
# Degrades to an empty client/ (404 update channel) — never a wrong
|
# Degrades to an empty client/ (404 update channel) — never a wrong
|
||||||
# version — if no release / APK asset / tag-count can be resolved.
|
# version — if no release / APK asset / tag-count can be resolved.
|
||||||
if: steps.guard.outputs.ready == 'true' && !startsWith(github.ref, 'refs/tags/v')
|
if: steps.guard.outputs.ready == 'true' && !startsWith(github.ref, 'refs/tags/v')
|
||||||
@@ -331,11 +370,20 @@ jobs:
|
|||||||
if [ -z "${TAG}" ] || [ -z "${APK_URL}" ]; then
|
if [ -z "${TAG}" ] || [ -z "${APK_URL}" ]; then
|
||||||
echo "::notice::latest release '${TAG:-?}' has no APK asset — image ships without bundled APK"; exit 0
|
echo "::notice::latest release '${TAG:-?}' has no APK asset — image ships without bundled APK"; exit 0
|
||||||
fi
|
fi
|
||||||
COUNT="$(git rev-list --count "${TAG}" 2>/dev/null || true)"
|
# Reconstruct the bundled APK's name with the SAME derivation
|
||||||
if [ -z "${COUNT}" ]; then
|
# android-release uses — commit timestamp of the tagged commit. The
|
||||||
echo "::notice::could not resolve commit count for ${TAG} (tag not fetched?) — skipping APK bundle"; exit 0
|
# two must agree exactly: this string is what the server hands
|
||||||
|
# clients to compare against what is installed, so a mismatch here
|
||||||
|
# is an update offered forever or never offered at all.
|
||||||
|
#
|
||||||
|
# This duplication is temporary. Once the tag itself becomes
|
||||||
|
# `v<version-name>`, this whole block collapses to `${TAG#v}` with
|
||||||
|
# nothing to recompute and nothing to keep in step.
|
||||||
|
COMMIT_TS="$(git log --format=%ct -1 "${TAG}" 2>/dev/null || true)"
|
||||||
|
if [ -z "${COMMIT_TS}" ]; then
|
||||||
|
echo "::notice::could not resolve commit timestamp for ${TAG} (tag not fetched?) — skipping APK bundle"; exit 0
|
||||||
fi
|
fi
|
||||||
VERSION_NAME="${TAG#v}.${COUNT}"
|
VERSION_NAME="$(date -u -d "@${COMMIT_TS}" +%Y.%m.%d.%H%M)"
|
||||||
curl -fsSL -H "Authorization: token ${CI_TOKEN}" -o client/minstrel.apk "${APK_URL}"
|
curl -fsSL -H "Authorization: token ${CI_TOKEN}" -o client/minstrel.apk "${APK_URL}"
|
||||||
echo "${VERSION_NAME}" > client/minstrel.apk.version
|
echo "${VERSION_NAME}" > client/minstrel.apk.version
|
||||||
echo "::notice::bundled release APK ${TAG} as version ${VERSION_NAME}"
|
echo "::notice::bundled release APK ${TAG} as version ${VERSION_NAME}"
|
||||||
|
|||||||
@@ -21,13 +21,24 @@ android {
|
|||||||
applicationId = "com.fabledsword.minstrel"
|
applicationId = "com.fabledsword.minstrel"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
// versionName / versionCode are released-build values injected by
|
// versionName / versionCode are released-build values injected by CI.
|
||||||
// CI from the git tag + commit count. Local / debug builds fall
|
// Local / debug builds fall back to "dev" so the About card reads
|
||||||
// back to "dev" so the About card reads honestly. Releases ship
|
// honestly.
|
||||||
// versionName="YYYY.MM.DD.<commits>" (e.g. "2026.06.02.142") and
|
//
|
||||||
// versionCode=<commits>, which is monotonic forever and lets the
|
// versionName is "YYYY.MM.DD.HHMM" from the COMMIT's timestamp, so
|
||||||
// shared isVersionNewer comparator distinguish two same-day
|
// every lane building this source reports the same string and the
|
||||||
// re-cuts (the iteration suffix differs).
|
// channel is the only thing that differs between them.
|
||||||
|
//
|
||||||
|
// versionCode is minutes since 2020-01-01 at BUILD time. It is the
|
||||||
|
// value the platform decides installs by, so it must be monotonic by
|
||||||
|
// construction.
|
||||||
|
//
|
||||||
|
// This comment used to say versionCode was a commit count and that it
|
||||||
|
// was "monotonic forever". It was neither — a commit count runs ahead
|
||||||
|
// on `dev`, so a dev build outranked the `main` release meant to
|
||||||
|
// replace it and Android refused the install as a downgrade. Worth
|
||||||
|
// knowing the claim was here, stated as a reassurance, while the bug
|
||||||
|
// it denied was live.
|
||||||
val versionNameOverride =
|
val versionNameOverride =
|
||||||
(project.findProperty("MINSTREL_VERSION_NAME") as String?)?.takeIf { it.isNotBlank() }
|
(project.findProperty("MINSTREL_VERSION_NAME") as String?)?.takeIf { it.isNotBlank() }
|
||||||
val versionCodeOverride =
|
val versionCodeOverride =
|
||||||
|
|||||||
Reference in New Issue
Block a user