Files
Serena Ruan aec304df87 fix(version): single source of truth for the omnigent version (#1772)
* fix(version): single source of truth for the omnigent version

The host and runner hard-coded version="0.1.0" in their hello frames,
so every host/runner reported a stale placeholder in the server's
version popover regardless of the build actually running. The server
had its own metadata->pyproject->PEP440 fallback to cope with installs
whose package metadata reports a non-PEP-440 "source" placeholder.

Introduce omnigent/version.py holding a single VERSION constant that the
runtime imports directly (no importlib.metadata round-trip), and wire the
host hello frame, runner hello frame, server /api/version, and CLI
--version to it. Importing the constant is correct regardless of how the
package was installed, so the server's fallback dance is deleted.

VERSION mirrors the canonical [project].version in pyproject.toml; a
pre-commit fixer (scripts/sync_version_py.py) rewrites the constant to
match pyproject and aborts the commit for re-staging on drift, so
releases stay a pyproject-only bump (via scripts/update_versions.py).

Co-authored-by: Isaac

* fix(version): teach the release bump path about omnigent/version.py

Polly review on #1772: the automated bump path (scripts/update_versions.py
+ .github/workflows/bump-version.yml) rewrote only the three pyproject.toml
files, never omnigent/version.py, and its `check` verified only the
pyprojects. A bot bump would therefore commit a stale VERSION constant and
trip the new test_version_matches_pyproject backstop — breaking the
"pyproject-only bump" story this change relies on.

Extend set_version() to also stamp the VERSION constant in
omnigent/version.py (anchored on its own `VERSION = "..."` line), and
extend check() to verify the constant equals the resolved [project].version
so a forgotten bump fails in the release tooling rather than on the bot PR.
The workflow's `git add -A` already picks up the extra file, so no YAML
logic change is needed — only the descriptive comment/PR body are updated.

Also soften sync_version_py.py's --check docstring, which implied a CI
wiring that never existed (per the review's non-blocking note).

Co-authored-by: Isaac

* test(version): don't assert /api/version against frozen package metadata

Polly review on #1772: the server version tests re-added
`== importlib.metadata.version("omnigent")` assertions. Since pyproject's
version is static (no dynamic wiring), that metadata is a frozen build-time
snapshot that can legitimately differ from VERSION — a stale editable
install or a "source" placeholder — the exact cases the removed server
fallback handled. Equality only holds right after a clean reinstall, so the
assertions are a latent spurious failure that undercuts the PR's
"authoritative regardless of how the package was installed" contract.

Drop the `_pkg_version` assertions in test_version_returns_source_of_truth_version
and test_info_includes_server_version (keep `== VERSION`), and remove the now
-unused import.

Also address non-blocking note 1: the --version banner (format_help) now reads
VERSION instead of importlib.metadata, for consistency with `--version`. The
upgrade path (cli.py) intentionally keeps reading installed metadata — it must
compare the on-disk install against PyPI.

Co-authored-by: Isaac
2026-07-01 17:31:20 +08:00

108 lines
3.8 KiB
Python

"""Sync ``omnigent/version.py``'s ``VERSION`` to the canonical pyproject version.
The root ``pyproject.toml``'s ``[project].version`` is the single source of
truth for the release version (stamped in lockstep with the SDK packages by
``scripts/update_versions.py``). The runtime, however, reads
``omnigent.version.VERSION`` — a plain constant it can import without touching
package metadata. This script keeps that constant equal to the canonical
pyproject version so the two never drift.
It is a pre-commit *fixer*: it rewrites the ``VERSION`` literal in place and
exits non-zero when it changed anything, so the commit aborts and the developer
re-stages the synced file (mirroring ``end-of-file-fixer`` and
``normalize_uv_lock_registry``).
Pass ``--check`` to validate without writing: it exits non-zero (and prints the
mismatch) when the constant is stale, but leaves the file untouched. (CI-side
drift is caught by ``scripts/update_versions.py check`` and the
``test_version_matches_pyproject`` test; this flag is for ad-hoc local use.)
Usage::
python scripts/sync_version_py.py # fix
python scripts/sync_version_py.py --check # verify
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
import tomllib
# scripts/sync_version_py.py -> repo root is one level up.
_REPO_ROOT = Path(__file__).resolve().parents[1]
_PYPROJECT = _REPO_ROOT / "pyproject.toml"
_VERSION_PY = _REPO_ROOT / "omnigent" / "version.py"
# The ``VERSION = "..."`` assignment (its own line) in omnigent/version.py.
_VERSION_ASSIGN = re.compile(r'^VERSION = "[^"]*"$', re.MULTILINE)
def _canonical_version() -> str:
"""Return ``[project].version`` from the root ``pyproject.toml``."""
return tomllib.loads(_PYPROJECT.read_text(encoding="utf-8"))["project"]["version"]
def _current_constant(text: str) -> str:
"""Return the ``VERSION`` literal currently in *text*.
:param text: Contents of ``omnigent/version.py``.
:returns: The quoted value of the ``VERSION`` assignment.
:raises ValueError: If the assignment is missing or not unique.
"""
matches = _VERSION_ASSIGN.findall(text)
if len(matches) != 1:
raise ValueError(
f'expected exactly one `VERSION = "..."` line in {_VERSION_PY}, found {len(matches)}'
)
return matches[0].split('"')[1]
def main(argv: list[str] | None = None) -> int:
"""Sync (or, with ``--check``, verify) the ``VERSION`` constant.
:param argv: Argument list (defaults to ``sys.argv[1:]``).
:returns: Process exit code — ``0`` when already in sync, ``1`` when a
rewrite was needed (fix mode) or a drift was found (check mode).
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--check",
action="store_true",
help="verify without writing; exit non-zero on drift",
)
# pre-commit passes the matched filenames; we operate on fixed paths, so
# accept and ignore them.
parser.add_argument("files", nargs="*", help=argparse.SUPPRESS)
args = parser.parse_args(argv)
canonical = _canonical_version()
text = _VERSION_PY.read_text(encoding="utf-8")
current = _current_constant(text)
if current == canonical:
return 0
if args.check:
print(
f"{_VERSION_PY.name}: VERSION is {current!r} but pyproject.toml is "
f"{canonical!r}; run `python scripts/sync_version_py.py` to fix",
file=sys.stderr,
)
return 1
new_text = _VERSION_ASSIGN.sub(f'VERSION = "{canonical}"', text)
_VERSION_PY.write_text(new_text, encoding="utf-8")
print(
f"{_VERSION_PY.name}: synced VERSION {current!r} -> {canonical!r} (re-stage the file)",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())