#!/usr/bin/env bash # # Derives the three values a build is stamped with, and the tag that names it. # # name=YYYY.MM.DD.HHMM label for people, from the COMMIT's timestamp # code= ordering key, minutes since 2020-01-01 at BUILD time # tag=v what a release of this commit must be called # # Usage: ci/version.sh [] (default HEAD) # # This exists as a script rather than inline workflow YAML for one reason: # release.yml only runs on `main` and on tags, so anything living inside it is # unverifiable until a release is already happening — which is the worst # possible moment to discover the version is wrong, because the failure mode # is silent (an update nobody is offered looks exactly like being current). # As a script it can be executed by a test on every push instead. # # The two clocks are deliberate and are NOT interchangeable: # # The NAME answers "is this the same code?" — so it must read identically on # every lane that builds this commit. Commit time does that; build time # prints two different strings for one thing. # # The CODE answers "may this be installed over that?" — so it must be # monotonic BY CONSTRUCTION. Build time is; commit time is not (rebuild an # older commit and it goes down, which on a phone is a refused install), and # a commit COUNT is worse still, because it runs ahead on `dev` and inverts # against `main`. set -euo pipefail readonly EPOCH_2020=1577836800 # 2020-01-01T00:00:00Z readonly REF="${1:-HEAD}" # Both clocks are overridable so a test can pin them. Nothing but tests should # set these — the defaults are the real derivation. commit_epoch="${MINSTREL_COMMIT_EPOCH:-}" if [ -z "${commit_epoch}" ]; then commit_epoch="$(git log --format=%ct -1 "${REF}")" fi now_epoch="${MINSTREL_NOW_EPOCH:-$(date -u +%s)}" if ! name="$(date -u -d "@${commit_epoch}" +%Y.%m.%d.%H%M 2>/dev/null)"; then echo "version.sh: could not read a commit timestamp from '${commit_epoch}'" >&2 exit 1 fi if ! [ "${now_epoch}" -eq "${now_epoch}" ] 2>/dev/null; then echo "version.sh: build timestamp '${now_epoch}' is not a number" >&2 exit 1 fi code=$(( (now_epoch - EPOCH_2020) / 60 )) # Assert the shape here, at the source. A malformed name builds, signs and # publishes perfectly happily; it only surfaces later as an update channel # that has quietly stopped offering anything. if [[ ! "${name}" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}$ ]]; then echo "version.sh: name '${name}' is not YYYY.MM.DD.HHMM" >&2 exit 1 fi # A non-positive key means the build clock is set before 2020, and every # comparison downstream would be nonsense. if [ "${code}" -le 0 ]; then echo "version.sh: ordering key '${code}' is not positive — build clock wrong?" >&2 exit 1 fi # Android's versionCode is a signed 32-bit int and the platform refuses an APK # whose code exceeds it. At ~525k minutes a year this is four thousand years # away in normal operation, so the realistic cause is a build machine with a # badly wrong clock — which produces a code that is not merely too large but # also unreachably high, permanently blocking every real build that follows # from ever outranking it. Cheaper to refuse the build than to discover that # from a phone that will not update. readonly VERSION_CODE_CEILING=2147483647 if [ "${code}" -gt "${VERSION_CODE_CEILING}" ]; then echo "version.sh: ordering key '${code}' exceeds versionCode's int32 ceiling — build clock wrong?" >&2 exit 1 fi # KEY=VALUE, which is also exactly $GITHUB_OUTPUT's format. echo "name=${name}" echo "code=${code}" echo "tag=v${name}"