sandcastle-ng

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

commit bb3942da46a266c09a75a86900ddb9691d30d840
parent 3b30edc52a898651a2d9d66d852f0cb9ff6f693c
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon, 27 Jul 2026 18:10:37 +0200

add sandcastle-upgrade script

Diffstat:
MREADME.md | 21+++++++++++++++++++++
Asandcastle-upgrade | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 179 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -30,6 +30,27 @@ Your host system should reverse-proxy HTTP(s) traffic to the respective service port for each of the services. +# Upgrading Component Versions + +Run `./sandcastle-upgrade` to update the git tags in `buildconfig/$component.tag` +to the latest upstream tag of the respective repository +(taken from `buildconfig/$component.giturl`). + +By default only production tags (`vX.Y.Z`, `deb-vX.Y.Z-R`) are considered. +Pass `--dev` to also consider dev tags (`vX.Y.Z-dev.N`). +Pass `--dry` to only show what would change. +Individual components can be given as arguments, e.g. + + ./sandcastle-upgrade --dev taler-exchange gnunet + +Components whose tag file pins something that is not a version tag +(such as a branch name) are left alone, as are components without a +`.giturl` file. + +To only *show* the latest upstream versions without changing anything, +use `./print-latest-versions`. + + # Building the Container Image 1. In `buildconfig/$component.tag` set the git tag you want to build. diff --git a/sandcastle-upgrade b/sandcastle-upgrade @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026 Taler Systems SA +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Upgrade the git tags in buildconfig/ to the latest upstream tag. +# Works like the "upgrade" subcommand of taler-pkg. + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + +# Tag syntax variants supported by buildscripts/sandcastle-build-generic: +# v$maj.$min.$patch => release version +# v$maj.$min.$patch-dev.$n => dev version +# deb-v$maj.$min.$patch-$revision => release version with debian revision +# Debian revisions of dev versions are *not* supported +tag_re_release = re.compile(r"v(\d+)\.(\d+)\.(\d+)") +tag_re_dev = re.compile(r"v(\d+)\.(\d+)\.(\d+)-dev\.(\d+)") +tag_re_deb = re.compile(r"deb-v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?") + + +def tag_sortkey(tag): + """Get a sort key for a tag, or None if the tag syntax isn't supported. + + Dev versions sort before the corresponding release version, debian + revisions sort after it. + """ + m = tag_re_release.fullmatch(tag) + if m: + return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, 0) + m = tag_re_dev.fullmatch(tag) + if m: + return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 0, int(m.group(4))) + m = tag_re_deb.fullmatch(tag) + if m: + rev = m.group(4) + return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, int(rev or 0)) + return None + + +def list_remote_tags(url): + """Get all tags from the git repo""" + cmd = ["git", "ls-remote", "--exit-code", "--refs", "--tags", url] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + tags = [] + for line in result.stdout.strip().split("\n"): + parts = line.split() + if len(parts) < 2: + continue + # refs/tags/v1.0.0 -> v1.0.0 + tags.append(parts[1].split("/")[-1]) + return tags + + +def latest_tag(tags, dev): + """Find the newest supported tag, only considering dev tags if dev is set""" + best = None + bestkey = None + # Iterate in sorted order, so that the result doesn't depend on + # the order in which the remote lists its refs. + for tag in sorted(tags): + if not dev and tag_re_dev.fullmatch(tag): + continue + key = tag_sortkey(tag) + if key is None: + continue + if bestkey is None or key > bestkey: + best = tag + bestkey = key + return best + + +def upgrade(cfg): + """Upgrade tag files in buildconfig to the latest upstream tag""" + names = cfg.components + if not names: + names = sorted(p.stem for p in Path("buildconfig").glob("*.tag")) + # Multiple components can share a repo, only ask each remote once. + remote_tags = {} + upgraded = [] + for name in names: + tag_file = Path("buildconfig") / f"{name}.tag" + url_file = Path("buildconfig") / f"{name}.giturl" + if not url_file.exists(): + print(f"[?] {name} has no giturl, skipping", file=sys.stderr) + continue + giturl = url_file.read_text().strip() + if giturl not in remote_tags: + remote_tags[giturl] = list_remote_tags(giturl) + latest = latest_tag(remote_tags[giturl], cfg.dev) + if latest is None: + print(f"[?] {name} has no usable tag in {giturl}, skipping", file=sys.stderr) + continue + curr = None + if tag_file.exists(): + curr = tag_file.read_text().strip() + currkey = tag_sortkey(curr) + if currkey is None: + print( + f"[?] {name} tag {curr} has unsupported syntax, skipping", + file=sys.stderr, + ) + continue + latestkey = tag_sortkey(latest) + if currkey > latestkey: + # Happens when the tag file pins a dev version but only + # production tags are considered. + print(f" {name} {curr} (newer than latest {latest})") + continue + if currkey == latestkey: + print(f" {name} {curr} (up to date)") + continue + print(f"[!] {name} {curr or '(none)'} -> {latest}") + upgraded.append(name) + if not cfg.dry: + tag_file.write_text(latest + "\n") + if not upgraded: + print("nothing to upgrade") + elif cfg.dry: + print("would upgrade:", " ".join(upgraded)) + else: + print("upgraded:", " ".join(upgraded)) + + +def main(): + parser = argparse.ArgumentParser( + prog="sandcastle-upgrade", + description="Upgrade component tags to the latest upstream version.", + ) + parser.add_argument( + "components", + nargs="*", + help="Components to upgrade (default: all components in buildconfig)", + ) + parser.add_argument( + "--dev", + help="Also consider dev tags, not just production tags", + action="store_true", + default=False, + ) + parser.add_argument("--dry", help="Dry run", action="store_true", default=False) + + args = parser.parse_args() + + # buildconfig/ is relative to this script, so the script can be + # run from any working directory. + os.chdir(os.path.dirname(os.path.realpath(__file__))) + + upgrade(args) + + +if __name__ == "__main__": + main()