commit bb3942da46a266c09a75a86900ddb9691d30d840
parent 3b30edc52a898651a2d9d66d852f0cb9ff6f693c
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 27 Jul 2026 18:10:37 +0200
add sandcastle-upgrade script
Diffstat:
| M | README.md | | | 21 | +++++++++++++++++++++ |
| A | sandcastle-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()