Files
flamenet-messenger-android/scripts/release-manifest.sh
T
Paul Hitt ad7c63ff4f An APK no longer ships without a word about what changed in it
Publishing was three steps done by hand, and only two of them left a trace.
The manifest and the APK could go out with nothing anywhere saying what was in
the build — which is how 0.8 shipped with notes and 0.9 shipped with none.

The gate goes on the manifest rather than on a checklist, because the manifest
is the step that cannot be skipped: it is what the in-app updater reads, so an
APK published without it offers nobody an update. A checklist item is skipped
on exactly the busy evening the notes matter most.

🔴 It matches on the `versionName` read out of the APK, never a number typed
here — the same reason this script already reads the version rather than
accepting one. The two cannot disagree.

The check itself lives in the relay repository (Scripts/covers-android.py),
because that is what serves the /releases page and defines the format. Parsing
the JSON here would leave a second copy of the schema to drift out of step
with the first, and a drifted gate is a gate that passes everything.

⚠️ FN_SKIP_RELEASE_NOTES=1 still publishes, and says so loudly on stderr — a
skipped gate that printed nothing would be a gate nobody remembered turning
off. A missing relay checkout is a refusal, not a pass, and names FN_RELAY_DIR.

Proven on a real APK in all four states: covered (manifest written, valid
JSON), uncovered (refused, and stdout stays EMPTY so no half-written manifest
lands), skipped, and no checkout. Every message goes to stderr, because this
script's stdout is the manifest itself.
2026-09-05 11:38:31 -04:00

109 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Write the update manifest for a built APK.
#
# 🔴 The version in the manifest is read back OUT of the APK, never typed. A
# manifest that claims a version the APK is not is worse than no manifest: it
# either offers an update that installs as the same build (and so appears to do
# nothing), or fails to offer one that exists. Reading it from the artifact is
# what makes the two incapable of disagreeing.
#
# Usage: scripts/release-manifest.sh <apk> [notes] > android-latest.json
set -euo pipefail
apk="${1:?usage: release-manifest.sh <apk> [notes]}"
notes="${2:-}"
[ -f "$apk" ] || { echo "no such APK: $apk" >&2; exit 1; }
# aapt2 ships with the build tools; find the newest one rather than pinning a
# version that will be wrong on the next machine.
sdk="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
aapt2="$(find "$sdk/build-tools" -maxdepth 2 -name aapt2 -type f 2>/dev/null | sort -V | tail -1)"
[ -n "$aapt2" ] || { echo "aapt2 not found under $sdk/build-tools" >&2; exit 1; }
badging="$("$aapt2" dump badging "$apk")"
code="$(sed -n "s/.*versionCode='\([0-9]*\)'.*/\1/p" <<<"$badging" | head -1)"
name="$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<<"$badging" | head -1)"
[ -n "$code" ] && [ -n "$name" ] || { echo "could not read version from $apk" >&2; exit 1; }
# ---------------------------------------------------------------------------
# 🔴 **A build does not ship without its release notes.**
#
# The gate lives here, on the manifest, because the manifest is the step that
# cannot be skipped — it is what the in-app updater reads, so an APK published
# without it offers nobody an update. A reminder in a checklist would be
# skipped on exactly the busy evening the notes matter most.
#
# ⚠️ It matches on `versionName` read out of the APK above, never on a number
# typed here, for the same reason the manifest does: the two cannot disagree.
#
# The changelog lives in the relay repository because that is what serves the
# page, and the check lives THERE too — see Scripts/covers-android.py. Parsing
# the JSON here would leave a second copy of the schema to drift.
if [ "${FN_SKIP_RELEASE_NOTES:-}" = "1" ]; then
# Loud, and on stderr: a skipped gate that printed nothing would be a gate
# nobody remembered turning off.
echo "⚠️ FN_SKIP_RELEASE_NOTES=1 — writing a manifest for $name with no" >&2
echo " release notes. Nothing on /releases will mention this build." >&2
else
relay_dir="${FN_RELAY_DIR:-$(cd "$(dirname "$0")/../.." && pwd)/flamenet-relay}"
checker="$relay_dir/Scripts/covers-android.py"
if [ ! -f "$checker" ]; then
cat >&2 <<MSG
No release-notes checker at:
$checker
That file lives in the flamenet-relay repository, which serves the /releases
page. Point FN_RELAY_DIR at your checkout of it, or set
FN_SKIP_RELEASE_NOTES=1 to publish $name with no notes.
MSG
exit 1
fi
if ! python3 "$checker" "$name"; then
cat >&2 <<MSG
Nothing on the /releases page mentions Android $name, so this build would ship
without a word to anyone about what changed in it.
Add a release to:
$relay_dir/Resources/releases.json
The newest release goes FIRST in the list, and it looks like this — write the
notes for somebody holding a phone, and put anything only an operator cares
about in "selfHosting" at the end:
{
"date": "$(date +%Y-%m-%d)",
"platforms": [{ "name": "android", "version": "$name" }],
"headline": "One sentence, or leave it out.",
"changes": [
{ "kind": "fixed", "text": "What a person will notice, in their words." }
]
}
"kind" is "new", "changed" or "fixed". Add an { "name": "ios", "version": … }
or { "name": "relay" } entry alongside it when the same release touched those
too — one release, every platform it landed on.
Then deploy the relay so the page and the APK go out together.
MSG
exit 1
fi
fi
sha="$(shasum -a 256 "$apk" | cut -d' ' -f1)"
# ⚠️ No `url` field, deliberately. The client builds the download URL from the
# relay it is signed in to and ignores anything the manifest says about where
# the APK lives — a relay that could name the host could hand its users a
# different app. Adding one here would be dead weight at best and a trap for
# the next person to implement a client at worst.
cat <<JSON
{
"versionCode": $code,
"versionName": "$name",
"sha256": "$sha",
"notes": $(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$notes")
}
JSON