Files
hermes-relay/scripts/check-plugin-version-sync.py

156 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Verify plugin-owned version metadata stays in sync.
Plugin releases use the `server-v*` track. The canonical version is
`pyproject.toml`'s `[project].version`; plugin and dashboard metadata should
match because they ship as one Hermes plugin package.
Android and CLI versions are intentionally separate release tracks and are
not checked here.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import re
import sys
import tomllib
from collections.abc import Callable
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
def _read_text(rel_path: str) -> str:
return (REPO_ROOT / rel_path).read_text(encoding="utf-8")
def _read_json(rel_path: str) -> object:
return json.loads(_read_text(rel_path))
def _pyproject_version() -> str:
data = tomllib.loads(_read_text("pyproject.toml"))
return str(data["project"]["version"])
def _regex_version(rel_path: str, pattern: str) -> str:
text = _read_text(rel_path)
match = re.search(pattern, text, re.MULTILINE)
if not match:
raise ValueError(f"{rel_path} has no parseable version")
return match.group(1)
def _json_version(rel_path: str, accessor: Callable[[object], object]) -> str:
value = accessor(_read_json(rel_path))
if not isinstance(value, str) or not value:
raise ValueError(f"{rel_path} has no parseable version")
return value
def _root_package_lock_version(data: object) -> object:
if not isinstance(data, dict):
raise ValueError("package-lock root is not an object")
packages = data.get("packages")
if not isinstance(packages, dict):
raise ValueError("package-lock has no packages object")
root_pkg = packages.get("")
if not isinstance(root_pkg, dict):
raise ValueError("package-lock has no root package entry")
return root_pkg.get("version")
def collect_versions() -> list[tuple[str, str]]:
return [
("pyproject.toml", _pyproject_version()),
(
"plugin/relay/__init__.py",
_regex_version(
"plugin/relay/__init__.py",
r'^__version__\s*=\s*"([^"]+)"',
),
),
(
"plugin/plugin.yaml",
_regex_version("plugin/plugin.yaml", r"^version:\s*([^\s#]+)"),
),
(
"plugin/dashboard/manifest.json",
_json_version(
"plugin/dashboard/manifest.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package.json",
_json_version(
"plugin/dashboard/package.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package-lock.json",
_json_version(
"plugin/dashboard/package-lock.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
'plugin/dashboard/package-lock.json packages[""]',
_json_version(
"plugin/dashboard/package-lock.json",
_root_package_lock_version,
),
),
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate plugin/dashboard version metadata."
)
parser.add_argument(
"--expect",
help="Require all plugin-owned metadata to match this exact version.",
)
args = parser.parse_args()
try:
versions = collect_versions()
except Exception as exc:
print(f"version sync check failed: {exc}", file=sys.stderr)
return 1
canonical = versions[0][1]
if args.expect and canonical != args.expect:
print(
f"pyproject.toml version {canonical!r} does not match expected "
f"{args.expect!r}",
file=sys.stderr,
)
return 1
width = max(len(label) for label, _ in versions)
for label, version in versions:
print(f"{label:<{width}} {version}")
mismatches = [(label, version) for label, version in versions if version != canonical]
if mismatches:
print(
f"plugin version metadata is out of sync; expected {canonical}",
file=sys.stderr,
)
for label, version in mismatches:
print(f" {label}: {version}", file=sys.stderr)
return 1
print(f"plugin version metadata is in sync: {canonical}")
return 0
if __name__ == "__main__":
raise SystemExit(main())