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