gabriel / musehub public
prune_releases.py python
95 lines 3.3 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 9 days ago
1 #!/usr/bin/env python3
2 """PEP 440-aware release pruning -- replaces `sort -V` in publish_muse_release.sh.
3
4 `sort -V` is a generic natural/version sort with zero knowledge of PEP 440's
5 reserved-segment precedence (dev < a < b < rc < final, see
6 docs/versioning.md's "Build channels" table). It compares "dev1" against
7 "rc14" as plain text, and 'd' sorts before 'r' -- placing the *newest* channel
8 (nightly) before older release candidates, so a naive keep-newest-N prune
9 deletes exactly the file that should have been kept. See MuseHub issue #128.
10
11 CLI usage (both cleanup call sites in publish_muse_release.sh consume this
12 identically, so the remote SSM host never re-implements version sorting):
13
14 python3 prune_releases.py --keep 3 muse-0.2.0rc14.tar.gz muse-0.2.0.dev1.tar.gz ...
15 """
16 from __future__ import annotations
17
18 import argparse
19 import re
20 import sys
21
22 from packaging.version import InvalidVersion, Version
23
24 _FILENAME_RE = re.compile(r"^muse-(?P<version>.+)\.tar\.gz$")
25
26
27 def stale_releases(filenames: list[str], keep: int) -> list[str]:
28 """Return filenames to delete -- all but the *keep* highest-precedence releases.
29
30 Ordered oldest-first (readable log output: the first line printed is the
31 first one deleted). Never guesses on an unparseable name: raises rather
32 than silently mis-sorting everything else around it.
33
34 Args:
35 filenames: Release tarball filenames, e.g. "muse-0.2.0rc14.tar.gz".
36 keep: Number of highest-precedence releases to retain.
37
38 Returns:
39 Filenames to delete, oldest (lowest-precedence) first. Empty if
40 ``keep >= len(filenames)``.
41
42 Raises:
43 ValueError: If any filename doesn't match the ``muse-{version}.tar.gz``
44 pattern, or its version segment isn't valid PEP 440.
45 """
46 if not filenames:
47 return []
48
49 parsed: list[tuple[Version, str]] = []
50 for name in filenames:
51 match = _FILENAME_RE.match(name)
52 if match is None:
53 raise ValueError(
54 f"Cannot parse release filename {name!r} -- expected "
55 "'muse-{version}.tar.gz'. Refusing to prune: a malformed "
56 "release name must stop the pipeline, not be silently "
57 "skipped or lexically mis-sorted."
58 )
59 try:
60 version = Version(match.group("version"))
61 except InvalidVersion as exc:
62 raise ValueError(
63 f"Release filename {name!r} has an invalid PEP 440 version "
64 f"segment {match.group('version')!r}: {exc}"
65 ) from exc
66 parsed.append((version, name))
67
68 parsed.sort(key=lambda pair: pair[0]) # oldest (lowest precedence) first
69
70 if keep >= len(parsed):
71 return []
72
73 stale_count = len(parsed) - keep
74 return [name for _version, name in parsed[:stale_count]]
75
76
77 def main(argv: list[str] | None = None) -> int:
78 parser = argparse.ArgumentParser(description=__doc__)
79 parser.add_argument("--keep", type=int, required=True, metavar="N")
80 parser.add_argument("filenames", nargs="*", metavar="FILENAME")
81 args = parser.parse_args(argv)
82
83 try:
84 stale = stale_releases(args.filenames, args.keep)
85 except ValueError as exc:
86 print(f"error: {exc}", file=sys.stderr)
87 return 1
88
89 for name in stale:
90 print(name)
91 return 0
92
93
94 if __name__ == "__main__":
95 raise SystemExit(main())
File History 2 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 9 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 9 days ago