gendesign/tradein-mvp/backend/app/core/version.py
lekss361 8423af5dd5
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Successful in 36s
Deploy Trade-In / build-frontend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m14s
Deploy Trade-In / build-backend (push) Successful in 4m19s
Deploy Trade-In / deploy (push) Successful in 1m44s
feat(tradein): версионирование продукта — единый источник, подвал, PDF, /versions (#2824)
2026-08-10 16:00:54 +00:00

83 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Product version metadata — единственный источник правды: `tradein-mvp/VERSION`.
`APP_VERSION` / `BUILD_SHA` / `BUILD_DATE` обычно приходят как runtime env,
запечённые в образ через build-args в `backend/Dockerfile`
(см. `.forgejo/workflows/deploy-tradein.yml`, job `build-backend`) — там же
ARG'и читают сам `VERSION`-файл, короткий `git rev-parse --short HEAD` и
`date -u +%Y-%m-%dT%H:%M:%SZ`.
Локальный запуск (`uvicorn app.main:app` без Docker-сборки) не задаёт эти env —
тогда версия читается напрямую из `VERSION` (поиск вверх по дереву каталогов,
см. `_find_version_file`), sha фолбэчит на `"dev"`, дата — на момент импорта
модуля. Ничего здесь не должно падать при отсутствии env (потребитель —
и PDF-колонтитул, и публичный `GET /api/v1/trade-in/version`).
Номер версии НЕ дублируется больше нигде в коде — читай `APP_VERSION` отсюда.
Раньше рядом существовали два независимых хардкода (`_REPORT_ENGINE_VERSION`
в trade_in_pdf.py, `ui-config.ts`'s `version` на фронте) — оба снесены, PDF и
`/trade-in/v2` теперь показывают ровно один номер, взятый из этого модуля /
`@/lib/buildInfo` соответственно; не заводи третий.
"""
from __future__ import annotations
import datetime as dt
import os
from pathlib import Path
_DEFAULT_VERSION = "0.0.0"
# Сколько уровней родителей проверять в поисках VERSION — с запасом покрывает
# и локальный layout (backend/app/core/version.py → ../../../VERSION ==
# tradein-mvp/VERSION, 3 уровня), и Docker runner layout (/app/app/core/
# version.py → /app/VERSION, 2 уровня, см. backend/Dockerfile COPY VERSION).
_MAX_ANCESTORS = 6
def _find_version_file() -> Path | None:
here = Path(__file__).resolve()
for ancestor in list(here.parents)[:_MAX_ANCESTORS]:
candidate = ancestor / "VERSION"
if candidate.is_file():
return candidate
return None
def _read_version_file() -> str:
path = _find_version_file()
if path is None:
return _DEFAULT_VERSION
try:
text = path.read_text(encoding="utf-8").strip()
except OSError:
return _DEFAULT_VERSION
return text or _DEFAULT_VERSION
def _default_build_date() -> str:
return dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
# Читаются один раз при импорте модуля (совпадает с паттерном `settings =
# Settings()` в app/core/config.py) — процесс живёт с одним образом/деплоем,
# перечитывать на каждый запрос незачем.
APP_VERSION: str = os.environ.get("APP_VERSION") or _read_version_file()
BUILD_SHA: str = os.environ.get("BUILD_SHA") or "dev"
BUILD_DATE: str = os.environ.get("BUILD_DATE") or _default_build_date()
def format_build_date_human(build_date: str = BUILD_DATE) -> str:
"""ISO-8601 UTC → `ДД.ММ.ГГГГ` для пользовательского отображения (PDF
колонтитул). Никогда не бросает исключение — при неразборчивой строке
возвращает её как есть (это футер отчёта, не API-контракт)."""
try:
parsed = dt.datetime.fromisoformat(build_date.replace("Z", "+00:00"))
except (ValueError, AttributeError):
return build_date
return parsed.strftime("%d.%m.%Y")
def product_version_line(product_name: str = "Мера") -> str:
"""`Мера v1.0.0 · a1b2c3d · 10.08.2026` — решение владельца продукта
2026-08-10 (SemVer + короткий SHA + дата сборки). Используется в PDF
колонтитуле; тот же набор значений отдаёт `GET /api/v1/trade-in/version`."""
return f"{product_name} v{APP_VERSION} · {BUILD_SHA} · {format_build_date_human()}"