media-updates/release.py
Richard 78f5a44e90 Give the download one permanent address
The manifest names the current artifact and the directory listing is off,
so "where do I download it by hand" could only be answered by reading the
manifest first. latest.apk is a symlink the release script repoints — git
stores it as a path, so it costs bytes rather than another 34MB a
release, and every past build stays reachable under its own name, which
is what lets a device midway through a download survive a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:01:31 +02:00

77 lines
2.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""Publish an APK: copy it in, write the manifest, commit.
Reads the version and build number out of amber-adult's pubspec so the two
cannot drift — a manifest advertising a build number the APK does not carry is
an update the device installs and is then offered again on every launch.
"""
import hashlib
import json
import pathlib
import re
import shutil
import subprocess
import sys
HERE = pathlib.Path(__file__).resolve().parent
PUBSPEC = HERE.parent / "amber-adult" / "pubspec.yaml"
BASE_URL = "https://media-updates.petruzalekr.cz"
def pubspec_version() -> tuple[str, int]:
m = re.search(r"^version:\s*([0-9.]+)\+([0-9]+)\s*$",
PUBSPEC.read_text(encoding="utf-8"), re.M)
if not m:
sys.exit(f"no version line in {PUBSPEC}")
return m.group(1), int(m.group(2))
def main() -> None:
if len(sys.argv) < 2:
sys.exit("usage: release.py <apk> [notes]")
apk = pathlib.Path(sys.argv[1]).resolve()
notes = sys.argv[2] if len(sys.argv) > 2 else ""
if not apk.is_file():
sys.exit(f"no such APK: {apk}")
version, build = pubspec_version()
public = HERE / "public"
public.mkdir(exist_ok=True)
name = f"media-{build}.apk"
dest = public / name
shutil.copy2(apk, dest)
digest = hashlib.sha256(dest.read_bytes()).hexdigest()
manifest = {
"version": version,
"buildNumber": build,
"notes": notes,
"sha256": digest,
"size": dest.stat().st_size,
"url": f"{BASE_URL}/{name}",
}
# ensure_ascii=False: the notes are Czech and get read by a person as
# often as by the app.
(public / "manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8")
# A stable name, so "where do I download it" has one permanent answer
# instead of one that has to be looked up in the manifest each time. A
# symlink rather than a copy: git stores it as a path, so it costs bytes
# rather than another 34MB per release.
latest = public / "latest.apk"
latest.unlink(missing_ok=True)
latest.symlink_to(name)
subprocess.run(["git", "add", "-A"], cwd=HERE, check=True)
subprocess.run(
["git", "commit", "-m", f"Release {version}+{build}"],
cwd=HERE, check=True)
print(f"staged {name} ({dest.stat().st_size // 1024 // 1024} MB), "
f"version {version}+{build}")
print("push to deploy")
if __name__ == "__main__":
main()