anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

debian-version.py (3578B)


      1 #!/usr/bin/env python3
      2 # This file is in the public domain.
      3 """Derive CI versions from the highest supported SemVer tag in the repository.
      4 
      5 Keep this helper and test_version.py identical across Buildbot Debian publishers.
      6 Only vX.Y.Z and vX.Y.Z-dev.K (optionally with build metadata) are release tags.
      7 Snapshots use X.Y.(Z+1)-0.N or X.Y.Z-dev.K.N in SemVer, and replace the
      8 prerelease separator with '~' and 'dev.' with 'dev' for Debian. Metadata and
      9 commit hashes do not affect either version. N counts tag..HEAD commits; only
     10 commit identity, not a zero count, makes a build an exact tagged release.
     11 """
     12 
     13 import re
     14 import subprocess
     15 import sys
     16 
     17 NUMBER = r"(0|[1-9][0-9]*)"
     18 TAG = re.compile(
     19     rf"v{NUMBER}\.{NUMBER}\.{NUMBER}(?:-dev\.{NUMBER})?"
     20     r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
     21 )
     22 
     23 
     24 def parse_tag(name):
     25     """Return a sortable precedence tuple, or None for an unsupported tag."""
     26     match = TAG.fullmatch(name)
     27     if match is None:
     28         return None
     29     major, minor, patch, dev, _ = match.groups()
     30     return (int(major), int(minor), int(patch), dev is None,
     31             0 if dev is None else int(dev))
     32 
     33 
     34 def select_tag(names):
     35     candidates = []
     36     for name in names:
     37         rank = parse_tag(name)
     38         if rank is None:
     39             print(f"Ignoring unsupported version tag: {name}", file=sys.stderr)
     40         else:
     41             candidates.append((rank, name))
     42     if not candidates:
     43         raise ValueError("no supported version tags (expected vX.Y.Z or vX.Y.Z-dev.K)")
     44     highest = max(rank for rank, _ in candidates)
     45     # Metadata is irrelevant to precedence. Prefer the plain tag, then the
     46     # lexically first tag name, independent of Git's configured tag sorting.
     47     return min((name for rank, name in candidates if rank == highest),
     48                key=lambda name: ("+" in name, name))
     49 
     50 
     51 def versions(tag, exact, count):
     52     major, minor, patch, stable, dev = parse_tag(tag)
     53     if stable:
     54         base = f"{major}.{minor}.{patch if exact else patch + 1}"
     55         suffix = "" if exact else f"0.{count}"
     56     else:
     57         base = f"{major}.{minor}.{patch}"
     58         suffix = f"dev.{dev}" + ("" if exact else f".{count}")
     59     semver = base + (f"-{suffix}" if suffix else "")
     60     debian = base + ("~" + suffix.replace("dev.", "dev", 1) if suffix else "")
     61     return semver, debian
     62 
     63 
     64 def git(*args):
     65     return subprocess.check_output(["git", *args], text=True).strip()
     66 
     67 
     68 def main():
     69     try:
     70         if git("rev-parse", "--is-shallow-repository") != "false":
     71             raise ValueError("a full Git checkout is required; disable shallow cloning")
     72         head = git("rev-parse", "HEAD")
     73         # Fetch the scheduled commit, including on detached HEADs, and all tags.
     74         # Do not impose a depth limit or move HEAD to the selected tag.
     75         subprocess.run(["git", "fetch", "--no-recurse-submodules", "--tags",
     76                         "origin", head], check=True, stdout=sys.stderr)
     77         tag = select_tag(git("tag", "--list").splitlines())
     78         tagged_commit = git("rev-parse", f"{tag}^{{commit}}")
     79         count = int(git("rev-list", "--count", f"{tag}..{head}"))
     80         semver, debian = versions(tag, head == tagged_commit, count)
     81         print(f"Version tag: {tag}; HEAD: {head}; commits: {count}; "
     82               f"SemVer: {semver}; Debian: {debian}", file=sys.stderr)
     83         print(debian)
     84         return 0
     85     except (OSError, subprocess.CalledProcessError, ValueError) as error:
     86         print(f"Cannot determine CI version: {error}", file=sys.stderr)
     87         return 1
     88 
     89 
     90 if __name__ == "__main__":
     91     sys.exit(main())