sandcastle-upgrade (5379B)
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 the git tags in buildconfig/ to the latest upstream tag. 8 # Works like the "upgrade" subcommand of taler-pkg. 9 10 import argparse 11 import os 12 import re 13 import subprocess 14 import sys 15 from pathlib import Path 16 17 # Tag syntax variants supported by buildscripts/sandcastle-build-generic: 18 # v$maj.$min.$patch => release version 19 # v$maj.$min.$patch-dev.$n => dev version 20 # deb-v$maj.$min.$patch-$revision => release version with debian revision 21 # Debian revisions of dev versions are *not* supported 22 tag_re_release = re.compile(r"v(\d+)\.(\d+)\.(\d+)") 23 tag_re_dev = re.compile(r"v(\d+)\.(\d+)\.(\d+)-dev\.(\d+)") 24 tag_re_deb = re.compile(r"deb-v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?") 25 26 27 def tag_sortkey(tag): 28 """Get a sort key for a tag, or None if the tag syntax isn't supported. 29 30 Dev versions sort before the corresponding release version, debian 31 revisions sort after it. 32 """ 33 m = tag_re_release.fullmatch(tag) 34 if m: 35 return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, 0) 36 m = tag_re_dev.fullmatch(tag) 37 if m: 38 return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 0, int(m.group(4))) 39 m = tag_re_deb.fullmatch(tag) 40 if m: 41 rev = m.group(4) 42 return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, int(rev or 0)) 43 return None 44 45 46 def list_remote_tags(url): 47 """Get all tags from the git repo""" 48 cmd = ["git", "ls-remote", "--exit-code", "--refs", "--tags", url] 49 result = subprocess.run(cmd, capture_output=True, text=True, check=True) 50 tags = [] 51 for line in result.stdout.strip().split("\n"): 52 parts = line.split() 53 if len(parts) < 2: 54 continue 55 # refs/tags/v1.0.0 -> v1.0.0 56 tags.append(parts[1].split("/")[-1]) 57 return tags 58 59 60 def latest_tag(tags, dev): 61 """Find the newest supported tag, only considering dev tags if dev is set""" 62 best = None 63 bestkey = None 64 # Iterate in sorted order, so that the result doesn't depend on 65 # the order in which the remote lists its refs. 66 for tag in sorted(tags): 67 if not dev and tag_re_dev.fullmatch(tag): 68 continue 69 key = tag_sortkey(tag) 70 if key is None: 71 continue 72 if bestkey is None or key > bestkey: 73 best = tag 74 bestkey = key 75 return best 76 77 78 def upgrade(cfg): 79 """Upgrade tag files in buildconfig to the latest upstream tag""" 80 names = cfg.components 81 if not names: 82 names = sorted(p.stem for p in Path("buildconfig").glob("*.tag")) 83 # Multiple components can share a repo, only ask each remote once. 84 remote_tags = {} 85 upgraded = [] 86 for name in names: 87 tag_file = Path("buildconfig") / f"{name}.tag" 88 url_file = Path("buildconfig") / f"{name}.giturl" 89 if not url_file.exists(): 90 print(f"[?] {name} has no giturl, skipping", file=sys.stderr) 91 continue 92 giturl = url_file.read_text().strip() 93 if giturl not in remote_tags: 94 remote_tags[giturl] = list_remote_tags(giturl) 95 latest = latest_tag(remote_tags[giturl], cfg.dev) 96 if latest is None: 97 print(f"[?] {name} has no usable tag in {giturl}, skipping", file=sys.stderr) 98 continue 99 curr = None 100 if tag_file.exists(): 101 curr = tag_file.read_text().strip() 102 currkey = tag_sortkey(curr) 103 if currkey is None: 104 print( 105 f"[?] {name} tag {curr} has unsupported syntax, skipping", 106 file=sys.stderr, 107 ) 108 continue 109 latestkey = tag_sortkey(latest) 110 if currkey > latestkey: 111 # Happens when the tag file pins a dev version but only 112 # production tags are considered. 113 print(f" {name} {curr} (newer than latest {latest})") 114 continue 115 if currkey == latestkey: 116 print(f" {name} {curr} (up to date)") 117 continue 118 print(f"[!] {name} {curr or '(none)'} -> {latest}") 119 upgraded.append(name) 120 if not cfg.dry: 121 tag_file.write_text(latest + "\n") 122 if not upgraded: 123 print("nothing to upgrade") 124 elif cfg.dry: 125 print("would upgrade:", " ".join(upgraded)) 126 else: 127 print("upgraded:", " ".join(upgraded)) 128 129 130 def main(): 131 parser = argparse.ArgumentParser( 132 prog="sandcastle-upgrade", 133 description="Upgrade component tags to the latest upstream version.", 134 ) 135 parser.add_argument( 136 "components", 137 nargs="*", 138 help="Components to upgrade (default: all components in buildconfig)", 139 ) 140 parser.add_argument( 141 "--dev", 142 help="Also consider dev tags, not just production tags", 143 action="store_true", 144 default=False, 145 ) 146 parser.add_argument("--dry", help="Dry run", action="store_true", default=False) 147 148 args = parser.parse_args() 149 150 # buildconfig/ is relative to this script, so the script can be 151 # run from any working directory. 152 os.chdir(os.path.dirname(os.path.realpath(__file__))) 153 154 upgrade(args) 155 156 157 if __name__ == "__main__": 158 main()