Add SemVer VERSION, Jenkins CI/CD, and release 0.1.1.

Ship Harbor/Hub push with latest+version tags, deploy to ovirt.devops.org.ru, and show the package version badge in Help → About.
This commit is contained in:
2026-07-28 00:56:52 +03:00
parent af26ad4141
commit 540675597a
18 changed files with 599 additions and 27 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ from app.lifespan import DatabaseFactory, create_lifespan, default_database_fact
from app.logging import configure_logging
from app.observability.health import router as health_router
from app.ovirt.mount import mount_ovirt_routes
from app.version import get_app_version
from app.web.routes import router as web_router
@@ -28,7 +29,7 @@ def create_app(
configure_logging(resolved.log_level)
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, ()),
)
+130
View File
@@ -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" / "ovirt-api-simulator" / "Chart.yaml"
_VALUES_FILE = _PROJECT_ROOT / "helm" / "ovirt-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("ovirt-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
+28 -1
View File
@@ -797,7 +797,12 @@
stroke: none;
}
.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;
@@ -811,6 +816,22 @@
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;
@@ -3835,6 +3856,7 @@
major: 45,
majors: [],
runtimeVersion: null,
appVersion: null,
catalog: null,
method: null,
pathValues: {},
@@ -5734,6 +5756,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">
@@ -5743,7 +5766,10 @@
automation, and infrastructure tooling without a real hypervisor cluster.
</p>
<p class="help-about-meta">
Runtime <code>${escapeHtml(version)}</code> · catalog <code>${escapeHtml(seriesLabel(catalogMajor))}</code>
<span class="help-about-version" title="Simulator package version">${escapeHtml(appVersion)}</span>
<span>
Runtime <code>${escapeHtml(version)}</code> · catalog <code>${escapeHtml(String(catalogMajor))}</code>
</span>
</p>
</div>
<div class="help-report-head">
@@ -6876,6 +6902,7 @@
// Accept both {majors:[…]} and a bare array for compatibility.
const majors = Array.isArray(p) ? p : (p.majors || []);
state.runtimeVersion = Array.isArray(p) ? null : (p.runtime_version || null);
state.appVersion = Array.isArray(p) ? null : (p.app_version || null);
state.majors = majors;
setText(els.statRuntime, state.runtimeVersion || "—");
const storedMajor = readStoredMajor();
+2
View File
@@ -12,6 +12,7 @@ from app.db.pool import AsyncpgDatabase
from app.dependencies import get_database
from app.ovirt.demo_datacenter import CLUSTER_SIZES, normalize_cluster_size, seed_ovirt_demo
from app.ovirt.seed import clear_ovirt_state, ovirt_demo_summary, seed_ovirt
from app.version import get_app_version
from app.web.assets import console_html
router = APIRouter(tags=["Simulator"])
@@ -47,6 +48,7 @@ async def ui_versions(request: Request) -> JSONResponse:
majors = ovirt_series_majors(runtime_version)
return JSONResponse(
{
"app_version": get_app_version(),
"majors": majors,
"runtime_version": runtime_version,
"default_major": next(