Добавлено автоверсионирование SemVer и версия v0.1.1 в футере.

This commit is contained in:
Sergey Antropoff
2026-07-27 22:49:36 +03:00
parent 0a0ae1f1e6
commit be337c0d40
14 changed files with 327 additions and 51 deletions
+8 -2
View File
@@ -14,6 +14,7 @@ from app.config import get_settings
from app.db import SessionLocal
from app.services.settings_service import get_or_create_settings
from app.services.storage import storage
from app.version import get_app_version, get_app_version_label
OPENAPI_TAGS = [
{
@@ -73,9 +74,10 @@ def custom_openapi(app: FastAPI):
def create_app() -> FastAPI:
settings = get_settings()
docs_on = settings.is_docs_enabled
app_version = get_app_version()
app = FastAPI(
title=settings.app_name,
version="0.1.0",
version=app_version,
description=(
"Wrapped — zero-knowledge one-time encrypted drop.\n\n"
"Public wrap APIs are anonymous. Admin APIs require HTTP Basic."
@@ -97,10 +99,14 @@ def create_app() -> FastAPI:
app.include_router(admin.api_router)
templates = Jinja2Templates(directory="app/templates")
templates.env.globals["app_version"] = app_version
templates.env.globals["app_version_label"] = get_app_version_label()
admin.templates.env.globals["app_version"] = app_version
admin.templates.env.globals["app_version_label"] = get_app_version_label()
@app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
async def health():
return {"status": "ok", "service": settings.app_name}
return {"status": "ok", "service": settings.app_name, "version": app_version}
@app.get("/", include_in_schema=False)
async def index(request: Request):
+18
View File
@@ -886,6 +886,24 @@ html[data-theme="light"] .footer-pillars li {
margin: 0.85rem 0 0;
font-size: 0.8rem;
color: var(--muted);
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.footer-copy-left {
min-width: 0;
}
.footer-copy-version {
margin-left: auto;
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--muted);
opacity: 0.9;
white-space: nowrap;
}
.footer-copy a {
color: var(--accent-2);
+5 -2
View File
@@ -66,8 +66,11 @@
</li>
</ul>
<p class="footer-copy">
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
<span class="footer-copy-left">
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
</span>
<span class="footer-copy-version" title="Version">{{ app_version_label | default('v0.1.0') }}</span>
</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+127
View File
@@ -0,0 +1,127 @@
from __future__ import annotations
import re
from functools import lru_cache
from pathlib import Path
_VERSION_FILE = Path(__file__).resolve().parents[1] / "VERSION"
_PROJECT_ROOT = _VERSION_FILE.parent
_PYPROJECT_FILE = _PROJECT_ROOT / "pyproject.toml"
_CHART_FILE = _PROJECT_ROOT / "helm" / "wrapped" / "Chart.yaml"
_VALUES_FILE = _PROJECT_ROOT / "helm" / "wrapped" / "values.yaml"
_DEFAULT_VERSION = "0.1.0"
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
def _read_version_file(path: Path) -> str | None:
try:
text = path.read_text(encoding="utf-8").strip()
except OSError:
return None
if _VERSION_RE.match(text):
return text
return None
@lru_cache
def get_app_version() -> str:
for path in (
_VERSION_FILE,
Path("/app/VERSION"),
Path.cwd() / "VERSION",
):
found = _read_version_file(path)
if found:
return found
try:
from importlib.metadata import version
meta = version("wrapped")
if _VERSION_RE.match(meta):
return meta
except Exception:
pass
return _DEFAULT_VERSION
def get_app_version_label() -> str:
return f"v{get_app_version()}"
def parse_version(value: str) -> tuple[int, int, int]:
match = _VERSION_RE.match(value.strip())
if not match:
raise ValueError(f"Invalid semantic version: {value!r}")
return int(match.group(1)), int(match.group(2)), int(match.group(3))
def format_version(major: int, minor: int, patch: int) -> str:
return f"{major}.{minor}.{patch}"
def clear_version_cache() -> None:
get_app_version.cache_clear()
def write_project_version(version: str) -> None:
parse_version(version)
_VERSION_FILE.write_text(f"{version}\n", encoding="utf-8")
pyproject = _PYPROJECT_FILE.read_text(encoding="utf-8")
pyproject, count = re.subn(
r'^version = ".*"$',
f'version = "{version}"',
pyproject,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError("Failed to update pyproject.toml version")
_PYPROJECT_FILE.write_text(pyproject, encoding="utf-8")
chart = _CHART_FILE.read_text(encoding="utf-8")
chart, chart_count = re.subn(
r"^version: .*$",
f"version: {version}",
chart,
count=1,
flags=re.MULTILINE,
)
chart, app_count = re.subn(
r'^appVersion: ".*"$',
f'appVersion: "{version}"',
chart,
count=1,
flags=re.MULTILINE,
)
if chart_count != 1 or app_count != 1:
raise RuntimeError("Failed to update Chart.yaml version")
_CHART_FILE.write_text(chart, encoding="utf-8")
values = _VALUES_FILE.read_text(encoding="utf-8")
values, values_count = re.subn(
r'^(\s*tag:\s*)".*"$',
rf'\1"{version}"',
values,
count=1,
flags=re.MULTILINE,
)
if values_count != 1:
raise RuntimeError("Failed to update helm values.yaml image.tag")
_VALUES_FILE.write_text(values, encoding="utf-8")
clear_version_cache()
def bump_patch_version() -> str:
major, minor, patch = parse_version(get_app_version())
version = format_version(major, minor, patch + 1)
write_project_version(version)
return version
def bump_minor_version() -> str:
major, minor, patch = parse_version(get_app_version())
version = format_version(major, minor + 1, 0)
write_project_version(version)
return version