fix: make proxy upgrades version-aware

Derive source-tree versions from release history so headroom --version no longer reports stale project metadata.

Restart stale idle proxies after an upgrade, but leave active sessions running to avoid interrupting ongoing conversations.

Remove _version.py from version-sync ownership and make version verification catch package/plugin manifest drift.
This commit is contained in:
Tejas Chopra 2026-05-09 15:58:27 -07:00
parent 4a88c698ad
commit ea1f608e79
7 changed files with 399 additions and 36 deletions

View file

@ -34,7 +34,7 @@ def temp_project(tmp_path: Path) -> dict[str, Path]:
pyproject = root / "pyproject.toml"
pyproject.write_text('[project]\nversion = "0.5.25"\n')
# headroom/_version.py
# headroom/_version.py is runtime-derived and must not be rewritten by version-sync.
version_py = headroom / "_version.py"
version_py.write_text('"""Package version metadata."""\n\n__version__ = "0.5.25"\n')
@ -102,9 +102,9 @@ def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None:
pyproject_content = temp_project["pyproject"].read_text()
assert 'version = "0.7.0"' in pyproject_content
# Verify headroom/_version.py
# Verify headroom/_version.py is not a synced manifest.
version_py_content = temp_project["version_py"].read_text()
assert '__version__ = "0.7.0"' in version_py_content
assert '__version__ = "0.5.25"' in version_py_content
# Verify plugins/openclaw/package.json
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
@ -157,7 +157,7 @@ def test_bump_patch(temp_project: dict[str, Path]) -> None:
assert 'version = "0.5.26"' in pyproject_content
version_py_content = temp_project["version_py"].read_text()
assert '__version__ = "0.5.26"' in version_py_content
assert '__version__ = "0.5.25"' in version_py_content
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
assert openclaw_pkg["version"] == "0.5.26"
@ -187,7 +187,7 @@ def test_bump_minor(temp_project: dict[str, Path]) -> None:
assert 'version = "0.6.0"' in pyproject_content
version_py_content = temp_project["version_py"].read_text()
assert '__version__ = "0.6.0"' in version_py_content
assert '__version__ = "0.5.25"' in version_py_content
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
assert openclaw_pkg["version"] == "0.6.0"
@ -217,7 +217,7 @@ def test_bump_major(temp_project: dict[str, Path]) -> None:
assert 'version = "1.0.0"' in pyproject_content
version_py_content = temp_project["version_py"].read_text()
assert '__version__ = "1.0.0"' in version_py_content
assert '__version__ = "0.5.25"' in version_py_content
openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text())
assert openclaw_pkg["version"] == "1.0.0"

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Verify all package versions are in sync before publishing."""
"""Verify all package manifest versions are in sync before publishing."""
import json
from pathlib import Path
@ -12,26 +12,44 @@ except ImportError: # pragma: no cover - Python 3.10 fallback
ROOT = Path(__file__).parent.parent
def main():
def _read_json_version(path: Path) -> str:
with open(path, encoding="utf-8") as f:
return str(json.load(f)["version"])
def _read_marketplace_versions(path: Path) -> dict[str, str]:
with open(path, encoding="utf-8") as f:
payload = json.load(f)
versions: dict[str, str] = {}
metadata = payload.get("metadata")
if isinstance(metadata, dict):
versions[f"{path}:metadata"] = str(metadata.get("version"))
plugins = payload.get("plugins")
if isinstance(plugins, list):
for index, plugin in enumerate(plugins):
if isinstance(plugin, dict):
versions[f"{path}:plugins[{index}]"] = str(plugin.get("version"))
return versions
def main() -> None:
with open(ROOT / "pyproject.toml", "rb") as f:
py_ver = tomllib.load(f)["project"]["version"]
with open(ROOT / "plugins/openclaw/package.json") as f:
npm_openclaw_ver = json.load(f)["version"]
with open(ROOT / "sdk/typescript/package.json") as f:
npm_sdk_ver = json.load(f)["version"]
headroom_ver = (
(ROOT / "headroom" / "_version.py").read_text().split('__version__ = "')[1].split('"')[0]
)
versions = {
"pyproject.toml": py_ver,
"openclaw/package.json": npm_openclaw_ver,
"typescript/package.json": npm_sdk_ver,
"headroom/_version.py": headroom_ver,
"plugins/openclaw/package.json": _read_json_version(ROOT / "plugins/openclaw/package.json"),
"sdk/typescript/package.json": _read_json_version(ROOT / "sdk/typescript/package.json"),
"plugins/headroom-agent-hooks/.claude-plugin/plugin.json": _read_json_version(
ROOT / "plugins/headroom-agent-hooks/.claude-plugin/plugin.json"
),
"plugins/headroom-agent-hooks/.github/plugin/plugin.json": _read_json_version(
ROOT / "plugins/headroom-agent-hooks/.github/plugin/plugin.json"
),
}
versions.update(_read_marketplace_versions(ROOT / ".claude-plugin/marketplace.json"))
versions.update(_read_marketplace_versions(ROOT / ".github/plugin/marketplace.json"))
if not all(v == py_ver for v in versions.values()):
print("Version mismatch detected:")

View file

@ -37,18 +37,6 @@ def bump_version(version: str, bump_type: str) -> str:
return f"{major}.{minor}.{patch}"
def update_version_py(root: Path, version: str) -> None:
"""Update headroom/_version.py with new version."""
version_py_path = root / "headroom" / "_version.py"
content = version_py_path.read_text(encoding="utf-8")
updated = re.sub(
r'__version__ = "[^"]+"',
f'__version__ = "{version}"',
content,
)
version_py_path.write_text(updated, encoding="utf-8")
def update_package_json(file_path: Path, version: str) -> None:
"""Update a package.json version field."""
with open(file_path, encoding="utf-8") as f:
@ -178,7 +166,6 @@ def main() -> None:
# Update all versioned files
update_pyproject_version(args.root, version)
update_version_py(args.root, version)
update_openclaw_package_json(
args.root / "plugins" / "openclaw" / "package.json", version, version
)