sandcastle-ng

Scripts for the deployment of Sandcastle (GNU Taler)
Log | Files | Refs | README

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()