Files
meshcore-bot/modules/version_info.py

123 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""
Version resolution utilities for MeshCore Bot.
Centralizes runtime version lookup so bot command output, web viewer, and
services all report consistent version information.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
from pathlib import Path
def _normalize_tag(value: str | None) -> str | None:
if not value:
return None
value = value.strip()
if not value:
return None
return value if value.startswith("v") else f"v{value}"
def _safe_git_run(repo_root: Path, args: list[str]) -> str | None:
try:
result = subprocess.run(
["git", "-C", str(repo_root)] + args,
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
return None
out = (result.stdout or "").strip()
return out or None
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, OSError):
return None
def _read_version_file(repo_root: Path) -> str | None:
version_file = repo_root / ".version_info"
if not version_file.is_file():
return None
try:
with open(version_file, encoding="utf-8") as fh:
data = json.load(fh)
version = data.get("installer_version") or data.get("tag")
return _normalize_tag(version)
except (OSError, json.JSONDecodeError, AttributeError):
return None
def _read_pyproject_version(repo_root: Path) -> str | None:
pyproject_path = repo_root / "pyproject.toml"
if not pyproject_path.is_file():
return None
try:
text = pyproject_path.read_text(encoding="utf-8")
except OSError:
return None
in_project_section = False
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
in_project_section = line == "[project]"
continue
if not in_project_section:
continue
match = re.match(r'version\s*=\s*"([^"]+)"', line)
if match:
return _normalize_tag(match.group(1))
return None
def resolve_runtime_version(repo_root: Path | str) -> dict[str, str | None]:
"""Resolve version metadata and a single runtime display value.
Returns a dict with:
- baked: release-like version from env/.version_info/pyproject (v-prefixed)
- tag: same as baked for template compatibility
- branch, commit, date: git metadata when available
- display: final runtime version string
"""
root = Path(repo_root).resolve()
env_version = _normalize_tag(os.environ.get("MESHCORE_BOT_VERSION", "").strip())
file_version = _read_version_file(root)
pyproject_version = _read_pyproject_version(root)
baked = env_version or file_version or pyproject_version
branch = _safe_git_run(root, ["rev-parse", "--abbrev-ref", "HEAD"])
commit = _safe_git_run(root, ["rev-parse", "--short", "HEAD"])
date_raw = _safe_git_run(root, ["show", "-s", "--format=%ci", "HEAD"])
date = None
if date_raw:
# %ci format is "YYYY-MM-DD HH:MM:SS +TZ"; keep date only.
date = date_raw.split()[0] if " " in date_raw else date_raw
display: str | None
if branch and branch != "main" and commit:
display = f"{branch}-{commit}"
elif branch == "main" and baked:
display = baked
else:
# Fallbacks for non-git/runtime-constrained environments.
display = baked or (f"{branch}-{commit}" if branch and commit else None) or "unknown"
return {
"baked": baked,
"tag": baked,
"branch": branch,
"commit": commit,
"date": date,
"display": display,
}