sandcastle-upgrade (4558B)
1 #!/usr/bin/env python3 2 3 # Copyright (c) 2026 Taler Systems SA 4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 # SPDX-License-Identifier: GPL-3.0-or-later 6 7 """Upgrade package tags in packages.toml to their latest upstream versions.""" 8 9 import argparse 10 import os 11 import re 12 import subprocess 13 import sys 14 from dataclasses import replace 15 from pathlib import Path 16 17 SCRIPT_DIR = Path(__file__).resolve().parent 18 sys.path.insert(0, str(SCRIPT_DIR / "buildscripts")) 19 20 from package_config import ConfigError, load_config, write_config # noqa: E402 21 22 23 tag_re_release = re.compile(r"v(\d+)\.(\d+)\.(\d+)") 24 tag_re_dev = re.compile(r"v(\d+)\.(\d+)\.(\d+)-dev\.(\d+)") 25 tag_re_deb = re.compile(r"deb-v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?") 26 27 28 def tag_sortkey(tag): 29 match = tag_re_release.fullmatch(tag) 30 if match: 31 return (int(match.group(1)), int(match.group(2)), int(match.group(3)), 1, 0) 32 match = tag_re_dev.fullmatch(tag) 33 if match: 34 return ( 35 int(match.group(1)), 36 int(match.group(2)), 37 int(match.group(3)), 38 0, 39 int(match.group(4)), 40 ) 41 match = tag_re_deb.fullmatch(tag) 42 if match: 43 revision = match.group(4) 44 return ( 45 int(match.group(1)), 46 int(match.group(2)), 47 int(match.group(3)), 48 1, 49 int(revision or 0), 50 ) 51 return None 52 53 54 def list_remote_tags(url): 55 result = subprocess.run( 56 ["git", "ls-remote", "--exit-code", "--refs", "--tags", url], 57 capture_output=True, 58 text=True, 59 check=True, 60 ) 61 tags = [] 62 for line in result.stdout.strip().split("\n"): 63 parts = line.split() 64 if len(parts) >= 2: 65 tags.append(parts[1].split("/")[-1]) 66 return tags 67 68 69 def latest_tag(tags, dev): 70 best = None 71 bestkey = None 72 for tag in sorted(tags): 73 if not dev and tag_re_dev.fullmatch(tag): 74 continue 75 key = tag_sortkey(tag) 76 if key is not None and (bestkey is None or key > bestkey): 77 best = tag 78 bestkey = key 79 return best 80 81 82 def upgrade(cfg, config_path): 83 config = load_config(config_path) 84 names = cfg.components or sorted(config.packages) 85 unknown = sorted(set(names) - set(config.packages)) 86 if unknown: 87 raise ConfigError(f"unknown package(s): {', '.join(unknown)}") 88 89 remote_tags = {} 90 upgraded = [] 91 updated_packages = dict(config.packages) 92 for name in names: 93 package = config.packages[name] 94 giturl = config.repository_for(package).url 95 if giturl not in remote_tags: 96 remote_tags[giturl] = list_remote_tags(giturl) 97 latest = latest_tag(remote_tags[giturl], cfg.dev) 98 if latest is None: 99 print(f"[?] {name} has no usable tag in {giturl}, skipping", file=sys.stderr) 100 continue 101 currkey = tag_sortkey(package.tag) 102 if currkey is None: 103 print( 104 f"[?] {name} tag {package.tag} has unsupported syntax, skipping", 105 file=sys.stderr, 106 ) 107 continue 108 latestkey = tag_sortkey(latest) 109 if currkey > latestkey: 110 print(f" {name} {package.tag} (newer than latest {latest})") 111 continue 112 if currkey == latestkey: 113 print(f" {name} {package.tag} (up to date)") 114 continue 115 print(f"[!] {name} {package.tag} -> {latest}") 116 upgraded.append(name) 117 if not cfg.dry: 118 updated_packages[name] = replace(package, tag=latest) 119 120 if not upgraded: 121 print("nothing to upgrade") 122 elif cfg.dry: 123 print("would upgrade:", " ".join(upgraded)) 124 else: 125 write_config(config_path, replace(config, packages=updated_packages)) 126 print("upgraded:", " ".join(upgraded)) 127 128 129 def main(): 130 parser = argparse.ArgumentParser( 131 prog="sandcastle-upgrade", 132 description="Upgrade package tags to the latest upstream version.", 133 ) 134 parser.add_argument( 135 "components", 136 nargs="*", 137 help="Packages to upgrade (default: all packages in packages.toml)", 138 ) 139 parser.add_argument("--dev", action="store_true", help="Also consider dev tags") 140 parser.add_argument("--dry", action="store_true", help="Dry run") 141 args = parser.parse_args() 142 143 os.chdir(SCRIPT_DIR) 144 try: 145 upgrade(args, SCRIPT_DIR / "packages.toml") 146 except ConfigError as exc: 147 parser.error(str(exc)) 148 149 150 if __name__ == "__main__": 151 main()