Files
meshcore-bot/modules/version_info.py
T
agessaman 51ab6e50df feat(db-manager): enhance SQLite journal mode handling and validation
- Introduced a locking mechanism to ensure journal mode is initialized once per configuration section, preventing redundant database operations.
- Added a validation function to check for required repeater tables, raising an error with actionable messages if any are missing.
- Updated the DBManager to handle journal modes more efficiently, ensuring that only persistent modes are applied across connections.
- Refactored the RepeaterManager to utilize the new validation function, improving error handling during initialization.
- Enhanced tests to cover new journal mode behaviors and validate repeater table existence, ensuring robust database management.
2026-08-02 09:48:05 -07:00

179 lines
6.4 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
# A release-looking version: "v" then a digit. Distinguishes a real release from
# the installer's "dev-<sha>" fallback and from tag names like "nightly".
_RELEASE_VERSION_RE = re.compile(r"^v\d")
def _normalize_tag(value: str | None) -> str | None:
if not value:
return None
value = value.strip()
if not value:
return None
# Only version-looking values get the conventional "v" prefix. install-service.sh
# writes installer_version = "dev-<sha>" for untagged checkouts, and a repo may
# carry tags like "nightly" — prefixing those produced "vdev-abc1234"/"vnightly".
return f"v{value}" if value[0].isdigit() else value
def _is_release_version(value: str | None) -> bool:
"""True when `value` names an actual release, not a dev/branch identifier."""
return bool(value and _RELEASE_VERSION_RE.match(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 get_application_root() -> Path:
"""Return the installed/source directory containing the bot application.
Runtime configuration may live elsewhere (for example, ``/etc/meshcore-bot``
or ``/data/config``), so version metadata must be resolved relative to this
module rather than relative to the config file.
"""
return Path(__file__).resolve().parent.parent
def resolve_application_version() -> dict[str, str | None]:
"""Resolve version metadata for the running bot application."""
return resolve_runtime_version(get_application_root())
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: the release this checkout actually *is*, else None. Deliberately not
``baked``: a source checkout bakes in the in-progress pyproject version —
which on a release-prep commit names a tag that does not exist yet — and
reporting that as the running release is how the viewer footer and the
version command came to disagree. Only a value git corroborates (or an
explicit override) becomes a tag.
- branch, commit, date: git metadata when available (branch is None when
HEAD is detached)
- display: final runtime version string; the single value UI should show
"""
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
# Detached HEAD: rev-parse reports the literal string "HEAD", not a branch name.
detached = branch == "HEAD"
if detached:
branch = None
# The tag HEAD actually sits on, if any. Checked on every checkout shape rather
# than only detached ones: `main` two commits past v1.0.0 is not v1.0.0 (and a
# release-prep commit bumps pyproject *before* the tag exists), while a release
# branch parked exactly on the tag is.
exact_tag: str | None = None
if commit:
exact_tag = _normalize_tag(
_safe_git_run(root, ["describe", "--tags", "--exact-match", "HEAD"])
)
display: str | None
tag: str | None
if env_version:
# An explicit override is authoritative — pinning the reported version is
# the only reason to set it.
display = env_version
elif exact_tag:
display = exact_tag
elif branch and commit:
display = f"{branch}-{commit}"
elif detached and commit:
display = f"detached-{commit}"
else:
# No usable git metadata: an installed release, which writes .version_info.
display = baked or "unknown"
# `tag` is a release identity, so it only survives when the value looks like one.
tag = display if _is_release_version(display) else None
return {
"baked": baked,
"tag": tag,
"branch": branch,
"commit": commit,
"date": date,
"display": display,
}