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
83 lines
4.3 KiB
Python
83 lines
4.3 KiB
Python
"""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()}"
|