Files
wehub-resource-sync 3e779be6f3
CI / test (3.14, ubuntu-24.04-arm) (push) Waiting to run
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

175 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Verify that the project version string matches across all version-stamped files.
Runs during the lint stage of CI so a release-prep commit that forgets to bump
one of the version-stamped files (the original sin behind v0.5.2's mis-named
`winpodx_0.5.1_*.deb` assets, which shipped because `debian/changelog` stayed
at 0.5.1 while `pyproject.toml` moved to 0.5.2) fails CI before the tag is
pushed, not after.
Single source of truth: ``[project] version`` in ``pyproject.toml``.
Files that have to agree with it:
- ``debian/changelog`` (first entry: ``winpodx (X.Y.Z) ...``)
- ``packaging/rpm/winpodx.spec`` (``Version: X.Y.Z`` -- the local-build
cosmetic literal; OBS rewrites this from the tarball name at publish
time, but a stale literal still misleads anyone running a manual rpmbuild
and the v0.5.2 incident proved a missed bump on ONE packaging file ships)
Plus a round-trip sanity check: ``importlib.metadata.version("winpodx")``
matches when the package is actually installed (catches a broken install
that would otherwise silently report the wrong version at runtime, since
0.6.0 ``src/winpodx/__init__.py`` derives ``__version__`` from the package
metadata rather than hand-syncing a literal).
Exits 0 if everything agrees, 1 with a stamped diff otherwise. Read-only --
makes no changes to the tree.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# stdlib on 3.11+; tomli backfill on 3.9 / 3.10 (matches winpodx's own pattern).
try:
import tomllib # type: ignore[import-not-found]
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib # type: ignore[import-not-found, no-redef]
ROOT = Path(__file__).resolve().parents[2]
def pyproject_version() -> str:
data = tomllib.loads((ROOT / "pyproject.toml").read_text())
return data["project"]["version"]
def debian_version() -> str:
text = (ROOT / "debian" / "changelog").read_text()
m = re.match(r"winpodx \(([^)]+)\)", text)
if not m:
raise SystemExit("first debian/changelog entry doesn't match 'winpodx (X.Y.Z) ...'")
return m.group(1)
def spec_version() -> str:
text = (ROOT / "packaging" / "rpm" / "winpodx.spec").read_text()
m = re.search(r"^Version:\s+(\S+)\s*$", text, re.MULTILINE)
if not m:
raise SystemExit("Version: line not found in packaging/rpm/winpodx.spec")
return m.group(1)
def installed_metadata_version() -> str | None:
"""Return ``importlib.metadata.version('winpodx')`` if winpodx is installed.
``None`` when winpodx isn't on ``sys.path`` as an installed package -- a
fresh source checkout running this script before ``pip install -e .`` is a
legitimate state; we don't want CI to flap on a tooling-only run.
"""
try:
from importlib.metadata import PackageNotFoundError, version
try:
return version("winpodx")
except PackageNotFoundError:
return None
except ImportError:
return None
def _sync_spec(version: str) -> bool:
"""Stamp ``Version: <version>`` into the RPM spec. True if it changed."""
path = ROOT / "packaging" / "rpm" / "winpodx.spec"
text = path.read_text()
new = re.sub(r"^(Version:\s+)\S+\s*$", rf"\g<1>{version}", text, count=1, flags=re.MULTILINE)
if new != text:
path.write_text(new)
return True
return False
def _sync_debian(version: str) -> bool:
"""Prepend a debian/changelog entry for ``version`` if the top entry differs.
Reuses the maintainer from the current top entry and stamps an RFC-2822
timestamp; the bullet is a generic pointer to CHANGELOG.md that the release
author can refine. True if an entry was added.
"""
if debian_version() == version:
return False
path = ROOT / "debian" / "changelog"
text = path.read_text()
m = re.search(r"^ -- (.+?) ", text, re.MULTILINE)
maintainer = m.group(1) if m else "Kim DaeHyun <kernalix7@kodenet.io>"
from datetime import datetime, timezone
from email.utils import format_datetime
stamp = format_datetime(datetime.now(timezone.utc))
entry = (
f"winpodx ({version}) unstable; urgency=medium\n\n"
f" * Release {version}. See CHANGELOG.md for details.\n\n"
f" -- {maintainer} {stamp}\n\n"
)
path.write_text(entry + text)
return True
def main() -> int:
import argparse
ap = argparse.ArgumentParser(description="Verify (or --write) version stamps.")
ap.add_argument(
"--write",
action="store_true",
help="stamp pyproject's version into debian/changelog + the RPM spec, "
"then verify -- run this during release prep so the stamps can't drift",
)
args = ap.parse_args()
pv = pyproject_version()
if args.write:
changed = [
name
for name, did in (
("packaging/rpm/winpodx.spec", _sync_spec(pv)),
("debian/changelog", _sync_debian(pv)),
)
if did
]
print(
f"Stamped {pv} into: {', '.join(changed)} (refine the debian/changelog bullet)"
if changed
else f"Already at {pv}; nothing to stamp."
)
versions = {
"pyproject.toml [project] version": pv,
"debian/changelog (first entry)": debian_version(),
"packaging/rpm/winpodx.spec Version:": spec_version(),
}
metadata = installed_metadata_version()
if metadata is not None:
versions["importlib.metadata installed version"] = metadata
unique = set(versions.values())
if len(unique) == 1:
print(f"Version stamps consistent: {unique.pop()}")
return 0
print("Version stamp mismatch -- release prep incomplete:")
for path, v in versions.items():
print(f" {path:46s} {v}")
print(
"\nBump the lagging file(s) and re-run before tagging.\n"
" pyproject.toml is the single source of truth; everything else must follow.\n"
" See `chore(release): vX.Y.Z` commits on main for the convention."
)
return 1
if __name__ == "__main__":
sys.exit(main())