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.
109 lines
4.4 KiB
Bash
Executable File
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
|