Keep only the last three builds

The repository grew ~64MB a release and never gave any of it back: at build
14 that was 762MB in public/ and 590MB in .git, stored twice on the VPS. The
cost nobody had counted was the image — public/ is copied into it, so a deploy
built 1.3GB of nginx, and Coolify keeps the old ones for rollback. That is
what filled the server's root filesystem and failed a push with "no space
left on device".

Nothing ever asks for an older build on purpose, so release.py now deletes
all but the newest three of each kind once the new one is staged. Each kind is
counted on its own: Linux is published rarely, and latest-linux.tar.gz has to
keep resolving through the releases that skip it. Both symlinks point at the
build just staged, which is the highest numbered one, so they survive the
prune by construction and /manifest.json never named anything but the newest.

This trims the working tree to 200MB. It does not shrink .git, which still
holds all fourteen builds; pruning that rewrites history and is a separate
decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-10-31 01:59:21 +01:00
parent 4bcd172629
commit 798013a957
3 changed files with 49 additions and 7 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# release.py runs "git add -A"; nothing here belongs in a release commit.
__pycache__/

View file

@ -22,10 +22,13 @@ It reads the version and build number out of `amber-adult/pubspec.yaml`, copies
the APK in under a versioned name, writes `manifest.json`, and commits. Push, the APK in under a versioned name, writes `manifest.json`, and commits. Push,
and Coolify redeploys. and Coolify redeploys.
**The APKs are committed.** At ~34MB and a handful of releases a year that is **The APKs are committed**, and only the newest `KEEP_BUILDS` (3) of each
the cheaper trade against standing up artifact storage for one app; if the kind are kept — the release script deletes the rest. At ~34MB each that is the
repository ever gets uncomfortable, the history is disposable — only the cheaper trade against standing up artifact storage for one app, but only while
newest artifact is ever fetched. the working tree stays small: every artifact in `public/` is also copied into
the image nginx serves, so a release used to build a 1.3GB image. Trimming
does not shrink `.git`, which still holds every past build; that history is
disposable, since only the newest artifact is ever fetched.
## Downloading by hand ## Downloading by hand
@ -35,9 +38,10 @@ newest artifact is ever fetched.
| Linux x64 | `https://media-updates.petruzalekr.cz/latest-linux.tar.gz` | | Linux x64 | `https://media-updates.petruzalekr.cz/latest-linux.tar.gz` |
Both are symlinks the release script repoints, so the addresses never change. Both are symlinks the release script repoints, so the addresses never change.
Every past build stays reachable under its own `media-<build>.apk` / The last three builds of each kind stay reachable under their own
`media-linux-<build>.tar.gz` name, which is what lets a device midway through a `media-<build>.apk` / `media-linux-<build>.tar.gz` names, which is what lets a
download survive a release. device midway through a download survive a release. Older ones are gone from
`public/` and answer 404.
The Linux tarball unpacks to `amber-adult/`; run `./amber-adult/amber_adult`. The Linux tarball unpacks to `amber-adult/`; run `./amber-adult/amber_adult`.
**It needs libmpv on the system** — media_kit loads it at runtime rather than **It needs libmpv on the system** — media_kit loads it at runtime rather than

View file

@ -17,6 +17,12 @@ import tarfile
HERE = pathlib.Path(__file__).resolve().parent HERE = pathlib.Path(__file__).resolve().parent
PUBSPEC = HERE.parent / "amber-adult" / "pubspec.yaml" PUBSPEC = HERE.parent / "amber-adult" / "pubspec.yaml"
BASE_URL = "https://media-updates.petruzalekr.cz" BASE_URL = "https://media-updates.petruzalekr.cz"
# How many builds of each kind stay in the repository. A device midway through
# a download has to be able to finish it, so the newest is never published
# alone; past that they are only weight, because nothing ever asks for an older
# build on purpose. Each one is ~34MB in git, again in the image nginx serves,
# and again in every clone of the repository.
KEEP_BUILDS = 3
def pubspec_version() -> tuple[str, int]: def pubspec_version() -> tuple[str, int]:
@ -27,6 +33,27 @@ def pubspec_version() -> tuple[str, int]:
return m.group(1), int(m.group(2)) return m.group(1), int(m.group(2))
def prune_old(public: pathlib.Path, prefix: str, suffix: str,
keep: int) -> list[str]:
"""Delete all but the `keep` newest `<prefix><build><suffix>` artifacts.
The `latest` symlinks point at the build just staged, which is the highest
numbered one, so they survive this by construction.
"""
builds = {}
for path in public.glob(f"{prefix}*{suffix}"):
middle = path.name[len(prefix):len(path.name) - len(suffix)]
# Excludes the other kind: "media-*.apk" would otherwise match a
# "media-linux-*" name, whose middle is not a number.
if middle.isdigit():
builds[int(middle)] = path
dropped = []
for build in sorted(builds)[:-keep]:
dropped.append(builds[build].name)
builds[build].unlink()
return dropped
def main() -> None: def main() -> None:
args = sys.argv[1:] args = sys.argv[1:]
# Linux is optional and off by default. It is another ~34MB in the # Linux is optional and off by default. It is another ~34MB in the
@ -96,6 +123,15 @@ def main() -> None:
tar_latest.symlink_to(tar_name) tar_latest.symlink_to(tar_name)
print(f"staged {tar_name}") print(f"staged {tar_name}")
# After the new artifacts are in place, so the build just staged counts
# towards the window it is kept by. Each kind is counted separately: Linux
# is published rarely, and its symlink has to keep resolving through the
# releases that skip it.
dropped = (prune_old(public, "media-", ".apk", KEEP_BUILDS)
+ prune_old(public, "media-linux-", ".tar.gz", KEEP_BUILDS))
if dropped:
print(f"dropped {', '.join(dropped)}")
subprocess.run(["git", "add", "-A"], cwd=HERE, check=True) subprocess.run(["git", "add", "-A"], cwd=HERE, check=True)
subprocess.run( subprocess.run(
["git", "commit", "-m", f"Release {version}+{build}"], ["git", "commit", "-m", f"Release {version}+{build}"],