Add SemVer VERSION, Jenkins CI/CD, and release 0.1.1.
Ship Harbor/Hub push with latest+version tags, deploy to proxmox.devops.org.ru, and show the package version badge in Help → About.
This commit is contained in:
+2
-1
@@ -25,6 +25,7 @@ from app.tasks.lxc import lxc_handler
|
||||
from app.tasks.qemu import qemu_handler
|
||||
from app.tasks.repository import TaskRepository
|
||||
from app.tasks.worker import TaskWorker
|
||||
from app.version import get_app_version
|
||||
from app.web.routes import router as web_router
|
||||
|
||||
|
||||
@@ -117,7 +118,7 @@ def create_app(
|
||||
resolved_workers = (task_worker,)
|
||||
app = FastAPI(
|
||||
title=resolved.app_name,
|
||||
version="0.1.0",
|
||||
version=get_app_version(),
|
||||
openapi_tags=openapi_tag_metadata(),
|
||||
lifespan=create_lifespan(resolved, database_factory, resolved_workers or ()),
|
||||
)
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
"""SemVer source of truth: VERSION file (synced to pyproject + Helm)."""
|
||||
|
||||
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" / "proxmox-api-simulator" / "Chart.yaml"
|
||||
_VALUES_FILE = _PROJECT_ROOT / "helm" / "proxmox-api-simulator" / "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 (
|
||||
Path("/app/VERSION"),
|
||||
_VERSION_FILE,
|
||||
Path.cwd() / "VERSION",
|
||||
):
|
||||
found = _read_version_file(path)
|
||||
if found:
|
||||
return found
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
|
||||
meta = version("proxmox-api-simulator")
|
||||
if _VERSION_RE.match(meta):
|
||||
return meta
|
||||
except Exception:
|
||||
pass
|
||||
return _DEFAULT_VERSION
|
||||
|
||||
|
||||
def get_app_version_label() -> str:
|
||||
clear_version_cache()
|
||||
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
|
||||
@@ -102,8 +102,10 @@ def list_majors(
|
||||
*,
|
||||
runtime_version: str | None,
|
||||
settings: Settings | None = None,
|
||||
app_version: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"app_version": app_version,
|
||||
"runtime_version": runtime_version,
|
||||
"majors": [
|
||||
{
|
||||
|
||||
+26
-1
@@ -796,6 +796,10 @@
|
||||
}
|
||||
|
||||
.help-about-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.45rem 0.65rem;
|
||||
margin: 0;
|
||||
padding: 0 14px 12px;
|
||||
font-size: 11px;
|
||||
@@ -809,6 +813,21 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.help-about-version {
|
||||
flex-shrink: 0;
|
||||
padding: 0.22rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
color: var(--accent);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -3932,6 +3951,7 @@
|
||||
major: 9,
|
||||
majors: [],
|
||||
runtimeVersion: null,
|
||||
appVersion: null,
|
||||
catalog: null,
|
||||
method: null,
|
||||
pathValues: {},
|
||||
@@ -5797,6 +5817,7 @@
|
||||
function buildAboutHtml() {
|
||||
const version = state.runtimeVersion || "—";
|
||||
const catalogMajor = state.major || "—";
|
||||
const appVersion = state.appVersion ? `v${state.appVersion}` : "v—";
|
||||
return `
|
||||
<div class="help-about-panel">
|
||||
<div class="help-report-head">
|
||||
@@ -5806,7 +5827,10 @@
|
||||
automation, and infrastructure tooling without a real hypervisor cluster.
|
||||
</p>
|
||||
<p class="help-about-meta">
|
||||
Runtime <code>${escapeHtml(version)}</code> · catalog PVE <code>${escapeHtml(String(catalogMajor))}</code>
|
||||
<span class="help-about-version" title="Simulator package version">${escapeHtml(appVersion)}</span>
|
||||
<span>
|
||||
Runtime <code>${escapeHtml(version)}</code> · catalog PVE <code>${escapeHtml(String(catalogMajor))}</code>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="help-report-head">
|
||||
@@ -6905,6 +6929,7 @@
|
||||
async function loadVersions() {
|
||||
const p = await fetch("/ui/api/versions").then((r) => r.json());
|
||||
state.runtimeVersion = p.runtime_version;
|
||||
state.appVersion = p.app_version || null;
|
||||
state.majors = p.majors || [];
|
||||
setText(els.statRuntime, p.runtime_version || "—");
|
||||
const storedMajor = readStoredMajor();
|
||||
|
||||
+8
-1
@@ -16,6 +16,7 @@ from app.contracts.source import SourceError
|
||||
from app.db.pool import AsyncpgDatabase
|
||||
from app.dependencies import get_database
|
||||
from app.simulation.seed import apply_seed, build_profile, simulation_state_summary
|
||||
from app.version import get_app_version
|
||||
from app.web.assets import console_html
|
||||
from app.web.compatibility_catalog import compatibility_payload
|
||||
from app.web.contract_catalog import catalog_payload, list_majors, load_snapshot, method_payload
|
||||
@@ -37,7 +38,13 @@ async def console() -> HTMLResponse:
|
||||
async def ui_versions(request: Request) -> JSONResponse:
|
||||
settings = _settings(request)
|
||||
runtime_version = _runtime_version(request)
|
||||
return JSONResponse(list_majors(runtime_version=runtime_version, settings=settings))
|
||||
return JSONResponse(
|
||||
list_majors(
|
||||
runtime_version=runtime_version,
|
||||
settings=settings,
|
||||
app_version=get_app_version(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ui/api/catalog", include_in_schema=False)
|
||||
|
||||
Reference in New Issue
Block a user