|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Bump every CoreAI UPM package to one version, in lockstep. |
| 3 | +
|
| 4 | +The repo ships six packages under Assets/*/ that release together. This sets the |
| 5 | +`version` field AND every internal `com.neoxider.*` dependency pin in each |
| 6 | +package.json to the target version, then verifies the result against the same |
| 7 | +rule the CI "Package graph (lockstep + deps)" gate enforces (one shared version, |
| 8 | +internal pins equal to it). Formatting and line endings are preserved (targeted |
| 9 | +regex, not a JSON re-dump). |
| 10 | +
|
| 11 | +Usage: |
| 12 | + python tools/bump_version.py 6.2.1 # write the bump to every package.json |
| 13 | + python tools/bump_version.py --check # verify lockstep only, no writes |
| 14 | +""" |
| 15 | +import glob |
| 16 | +import json |
| 17 | +import os |
| 18 | +import re |
| 19 | +import sys |
| 20 | + |
| 21 | +SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?$") |
| 22 | +VERSION_FIELD = re.compile(r'("version"\s*:\s*")[^"]*(")') |
| 23 | +INTERNAL_PIN = re.compile(r'("com\.neoxider\.[a-z0-9.]+"\s*:\s*")[^"]*(")') |
| 24 | + |
| 25 | + |
| 26 | +def package_files(): |
| 27 | + return sorted(glob.glob("Assets/*/package.json")) |
| 28 | + |
| 29 | + |
| 30 | +def bump_file(path, version): |
| 31 | + with open(path, encoding="utf-8", newline="") as f: |
| 32 | + text = f.read() |
| 33 | + text, n_version = VERSION_FIELD.subn(r"\g<1>" + version + r"\g<2>", text, count=1) |
| 34 | + text, n_pins = INTERNAL_PIN.subn(r"\g<1>" + version + r"\g<2>", text) |
| 35 | + with open(path, "w", encoding="utf-8", newline="") as f: |
| 36 | + f.write(text) |
| 37 | + return n_version, n_pins |
| 38 | + |
| 39 | + |
| 40 | +def check(): |
| 41 | + """Return True when every package shares one version and every internal pin matches it.""" |
| 42 | + pkgs = {} |
| 43 | + for path in package_files(): |
| 44 | + with open(path, encoding="utf-8") as f: |
| 45 | + data = json.load(f) |
| 46 | + pkgs[data["name"]] = (data.get("version", ""), data.get("dependencies", {})) |
| 47 | + |
| 48 | + ok = True |
| 49 | + versions = {v[0] for v in pkgs.values()} |
| 50 | + if len(versions) != 1: |
| 51 | + print(f"ERROR: package versions are not in lockstep: {sorted(versions)}") |
| 52 | + ok = False |
| 53 | + for name, (_ver, deps) in pkgs.items(): |
| 54 | + for dep, dver in deps.items(): |
| 55 | + if dep.startswith("com.neoxider.") and dep in pkgs and dver != pkgs[dep][0]: |
| 56 | + print(f"ERROR: {name} pins {dep}@{dver} but that package is {pkgs[dep][0]}") |
| 57 | + ok = False |
| 58 | + print("packages:", {n: v[0] for n, v in sorted(pkgs.items())}) |
| 59 | + return ok |
| 60 | + |
| 61 | + |
| 62 | +def main(argv): |
| 63 | + # Run from the repo root regardless of the caller's cwd. |
| 64 | + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) |
| 65 | + |
| 66 | + if not argv or argv[0] in ("-h", "--help"): |
| 67 | + print(__doc__) |
| 68 | + return 0 |
| 69 | + |
| 70 | + if argv[0] == "--check": |
| 71 | + return 0 if check() else 1 |
| 72 | + |
| 73 | + version = argv[0].lstrip("v") |
| 74 | + if not SEMVER.match(version): |
| 75 | + print(f"ERROR: '{version}' is not a valid semver (expected e.g. 6.2.1)") |
| 76 | + return 2 |
| 77 | + |
| 78 | + files = package_files() |
| 79 | + if not files: |
| 80 | + print("ERROR: no Assets/*/package.json found") |
| 81 | + return 2 |
| 82 | + |
| 83 | + total_pins = 0 |
| 84 | + for path in files: |
| 85 | + n_version, n_pins = bump_file(path, version) |
| 86 | + total_pins += n_pins |
| 87 | + print(f" {path}: version x{n_version}, internal pins x{n_pins}") |
| 88 | + print(f"bumped {len(files)} packages to {version} ({total_pins} internal pins)") |
| 89 | + |
| 90 | + if not check(): |
| 91 | + print("LOCKSTEP CHECK FAILED after bump") |
| 92 | + return 1 |
| 93 | + print(f"LOCKSTEP CHECK PASSED for {version}") |
| 94 | + print("Next: add a CHANGELOG entry, commit, tag, and release.") |
| 95 | + return 0 |
| 96 | + |
| 97 | + |
| 98 | +if __name__ == "__main__": |
| 99 | + sys.exit(main(sys.argv[1:])) |
0 commit comments