version_align.py python
163 lines 5.2 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Version alignment checker (§QR.8).
2
3 Verifies equality (after trim) of VERSION, desktop package.json, Cargo.toml,
4 tauri.conf.json, and the release tag / dispatch version. Pure — never writes.
5 """
6
7 from __future__ import annotations
8
9 import json
10 import re
11 from dataclasses import dataclass
12 from pathlib import Path
13
14
15 class VersionAlignError(ValueError):
16 """Raised when version strings are missing or mismatched."""
17
18
19 _CARGO_VERSION_RE = re.compile(
20 r'(?m)^\[package\]\s*\n(?:.*\n)*?^version\s*=\s*"([^"]+)"',
21 )
22
23
24 @dataclass(frozen=True)
25 class VersionSources:
26 """Collected version strings from kit tree files."""
27
28 root_version: str
29 package_json: str
30 cargo_toml: str
31 tauri_conf: str
32
33
34 def read_root_version(kit_root: Path) -> str:
35 """Return trimmed contents of ``VERSION``."""
36 path = kit_root / "VERSION"
37 if not path.is_file():
38 raise VersionAlignError(f"missing VERSION file: {path}")
39 return path.read_text(encoding="utf-8").strip()
40
41
42 def read_package_json_version(kit_root: Path) -> str:
43 """Return ``desktop/package.json`` version."""
44 path = kit_root / "desktop" / "package.json"
45 if not path.is_file():
46 raise VersionAlignError(f"missing package.json: {path}")
47 data = json.loads(path.read_text(encoding="utf-8"))
48 version = data.get("version")
49 if not isinstance(version, str) or not version.strip():
50 raise VersionAlignError("desktop/package.json missing version string")
51 return version.strip()
52
53
54 def read_cargo_toml_version(kit_root: Path) -> str:
55 """Return ``[package].version`` from desktop Cargo.toml."""
56 path = kit_root / "desktop" / "src-tauri" / "Cargo.toml"
57 if not path.is_file():
58 raise VersionAlignError(f"missing Cargo.toml: {path}")
59 text = path.read_text(encoding="utf-8")
60 match = _CARGO_VERSION_RE.search(text)
61 if match is None:
62 # Fallback: first package version line after [package]
63 in_package = False
64 for line in text.splitlines():
65 stripped = line.strip()
66 if stripped == "[package]":
67 in_package = True
68 continue
69 if in_package and stripped.startswith("[") and stripped.endswith("]"):
70 break
71 if in_package and stripped.startswith("version"):
72 _, _, raw = stripped.partition("=")
73 return raw.strip().strip('"').strip("'")
74 raise VersionAlignError("Cargo.toml missing package.version")
75 return match.group(1).strip()
76
77
78 def read_tauri_conf_version(kit_root: Path) -> str:
79 """Return ``version`` from tauri.conf.json."""
80 path = kit_root / "desktop" / "src-tauri" / "tauri.conf.json"
81 if not path.is_file():
82 raise VersionAlignError(f"missing tauri.conf.json: {path}")
83 data = json.loads(path.read_text(encoding="utf-8"))
84 version = data.get("version")
85 if not isinstance(version, str) or not version.strip():
86 raise VersionAlignError("tauri.conf.json missing version string")
87 return version.strip()
88
89
90 def collect_versions(kit_root: Path) -> VersionSources:
91 """Load all four tree-side version sources."""
92 return VersionSources(
93 root_version=read_root_version(kit_root),
94 package_json=read_package_json_version(kit_root),
95 cargo_toml=read_cargo_toml_version(kit_root),
96 tauri_conf=read_tauri_conf_version(kit_root),
97 )
98
99
100 def normalize_tag(tag: str) -> str:
101 """Strip a leading ``v`` from a git tag name."""
102 tag = tag.strip()
103 if tag.startswith("v") or tag.startswith("V"):
104 return tag[1:]
105 return tag
106
107
108 def check_version_alignment(
109 kit_root: Path,
110 *,
111 tag: str | None = None,
112 dispatch_version: str | None = None,
113 ) -> str:
114 """Fail closed unless all version sources equal.
115
116 Parameters
117 ----------
118 kit_root:
119 Repository root containing ``VERSION`` and ``desktop/``.
120 tag:
121 Optional git tag (``v0.1.0`` or ``0.1.0``). When set, must equal VERSION.
122 dispatch_version:
123 Optional ``workflow_dispatch`` version input. When set, must equal VERSION.
124
125 Returns
126 -------
127 str
128 The aligned version string.
129
130 Raises
131 ------
132 VersionAlignError
133 On any mismatch or missing file.
134 """
135 sources = collect_versions(kit_root)
136 values = {
137 "VERSION": sources.root_version,
138 "desktop/package.json": sources.package_json,
139 "desktop/src-tauri/Cargo.toml": sources.cargo_toml,
140 "desktop/src-tauri/tauri.conf.json": sources.tauri_conf,
141 }
142 expected = sources.root_version
143 for label, value in values.items():
144 if value != expected:
145 raise VersionAlignError(
146 f"version mismatch: {label}={value!r} != VERSION={expected!r}"
147 )
148
149 if tag is not None:
150 tag_version = normalize_tag(tag)
151 if tag_version != expected:
152 raise VersionAlignError(
153 f"version mismatch: git tag {tag!r} → {tag_version!r} != VERSION={expected!r}"
154 )
155
156 if dispatch_version is not None:
157 dv = dispatch_version.strip()
158 if dv != expected:
159 raise VersionAlignError(
160 f"version mismatch: dispatch version={dv!r} != VERSION={expected!r}"
161 )
162
163 return expected
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago