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:
Sergey Antropoff
2026-07-28 00:24:05 +03:00
parent f981ca1bf1
commit f81d6186ab
21 changed files with 610 additions and 19 deletions
+1
View File
@@ -27,6 +27,7 @@ ENV PATH="/opt/venv/bin:$PATH" \
RUN groupadd --system --gid 10001 simulator \
&& useradd --system --uid 10001 --gid simulator --home-dir /app --no-create-home simulator
COPY --from=builder /opt/venv /opt/venv
COPY VERSION /app/VERSION
COPY contracts/e61a893e996d05d376579226e7dfbedbcfce8b71787adacffbc557e6e35901c1/snapshot.json /app/contracts/pve-9.2.3.json
COPY evidence/ /app/evidence/
WORKDIR /app
Vendored
+203
View File
@@ -0,0 +1,203 @@
// Proxmox API Simulator — CI/CD: multi-arch push (Harbor + Docker Hub) + deploy
//
// Один файл: Build (DinD) → Deploy (kubectl set image в релиз simulators).
// Первый install lab — через DevOpsTools/K3S: make addon-simulators
// (Ingress https://proxmox.devops.org.ru уже из addon values).
//
// Версия образа = VERSION из коммита. Локальный bump: make bump-patch / make push.
// Теги: :{VERSION} и :latest (без sha).
//
// Credentials (Global):
// harbor-devops-tools-push-pull-access — Harbor devops-tools (robot)
// docker-hub — Docker Hub (inecs)
// k3s-kubeconfig — kubeconfig к K3S
// ssh-gitea-key — SCM checkout Gitea (в job)
pipeline {
agent none
options {
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
timeout(time: 60, unit: 'MINUTES')
timestamps()
}
environment {
HARBOR_REGISTRY = 'hub.antropoff.ru'
HARBOR_IMAGE = 'hub.antropoff.ru/devops-tools/proxmox-api-simulator'
DOCKERHUB_IMAGE = 'inecs/proxmox-api-simulator'
BUILDX_BUILDER = "jenkins-proxmox-api-simulator-${env.BUILD_NUMBER}"
HELM_NAMESPACE = 'simulators'
DEPLOYMENT_NAME = 'simulators-proxmox'
INGRESS_HOST = 'proxmox.devops.org.ru'
TZ = 'Europe/Moscow'
}
stages {
stage('Build & push') {
when {
anyOf {
branch 'main'
branch 'master'
}
}
agent { label 'docker' }
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Version') {
steps {
script {
env.IMAGE_VERSION = readFile('VERSION').trim()
if (!env.IMAGE_VERSION) {
error('VERSION file is empty')
}
echo "IMAGE_VERSION from VERSION → ${env.IMAGE_VERSION}"
}
}
}
stage('Buildx push') {
steps {
withCredentials([
usernamePassword(
credentialsId: 'harbor-devops-tools-push-pull-access',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
),
usernamePassword(
credentialsId: 'docker-hub',
usernameVariable: 'DOCKERHUB_USER',
passwordVariable: 'DOCKERHUB_PASS'
)
]) {
container('docker') {
sh '''
set -eux
test -n "${IMAGE_VERSION}"
echo "Building tags: ${IMAGE_VERSION}, latest (amd64+arm64)"
echo "$HARBOR_PASS" | docker login "$HARBOR_REGISTRY" -u "$HARBOR_USER" --password-stdin
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin
docker buildx rm "$BUILDX_BUILDER" 2>/dev/null || true
docker buildx create --name "$BUILDX_BUILDER" --driver docker-container --use
docker buildx inspect --bootstrap >/dev/null
docker buildx build \
--platform linux/amd64,linux/arm64 \
--target runtime \
--build-arg "APP_VERSION=${IMAGE_VERSION}" \
--provenance=false --sbom=false --push \
-t "${HARBOR_IMAGE}:${IMAGE_VERSION}" \
-t "${HARBOR_IMAGE}:latest" \
-t "${DOCKERHUB_IMAGE}:${IMAGE_VERSION}" \
-t "${DOCKERHUB_IMAGE}:latest" \
-f Dockerfile .
echo "--- Harbor ---"
docker buildx imagetools inspect "${HARBOR_IMAGE}:${IMAGE_VERSION}" | sed -n '1,40p'
echo "--- Docker Hub ---"
docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:${IMAGE_VERSION}" | sed -n '1,40p'
docker buildx rm "$BUILDX_BUILDER" || true
'''
}
}
}
}
stage('Deploy') {
steps {
withCredentials([
file(credentialsId: 'k3s-kubeconfig', variable: 'KUBECONFIG')
]) {
container('docker') {
sh '''
set -eux
if ! command -v kubectl >/dev/null 2>&1; then
apk add --no-cache curl >/dev/null
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
esac
KVER="$(curl -fsSL https://dl.k8s.io/release/stable.txt)"
curl -fsSLo /usr/local/bin/kubectl \
"https://dl.k8s.io/release/${KVER}/bin/linux/${ARCH}/kubectl"
chmod +x /usr/local/bin/kubectl
fi
kubectl version --client --short || kubectl version --client
if ! kubectl -n "$HELM_NAMESPACE" get deploy "$DEPLOYMENT_NAME" >/dev/null 2>&1; then
echo "Deployment ${HELM_NAMESPACE}/${DEPLOYMENT_NAME} not found." >&2
echo "Сначала: make addon-simulators (Ingress ${INGRESS_HOST})." >&2
exit 1
fi
echo "Rolling ${DOCKERHUB_IMAGE}:${IMAGE_VERSION} → ${HELM_NAMESPACE}/${DEPLOYMENT_NAME}"
echo "Public URL: https://${INGRESS_HOST}/"
# Main container always; migrate initContainer when present.
kubectl -n "$HELM_NAMESPACE" set image \
"deployment/${DEPLOYMENT_NAME}" \
"simulator=${DOCKERHUB_IMAGE}:${IMAGE_VERSION}"
if kubectl -n "$HELM_NAMESPACE" get deploy "$DEPLOYMENT_NAME" \
-o jsonpath='{.spec.template.spec.initContainers[*].name}' \
| tr ' ' '\n' | grep -qx migrate; then
kubectl -n "$HELM_NAMESPACE" set image \
"deployment/${DEPLOYMENT_NAME}" \
"migrate=${DOCKERHUB_IMAGE}:${IMAGE_VERSION}"
fi
kubectl -n "$HELM_NAMESPACE" rollout status \
"deployment/${DEPLOYMENT_NAME}" --timeout=300s
kubectl -n "$HELM_NAMESPACE" get deploy,po,ing -o wide
'''
}
}
}
}
}
post {
always {
script {
try {
container('docker') {
sh 'docker buildx rm "$BUILDX_BUILDER" 2>/dev/null || true'
}
} catch (Ignored) {
// pod may already be gone
}
}
}
success {
echo "✓ ${DOCKERHUB_IMAGE}:{${IMAGE_VERSION},latest} → https://${INGRESS_HOST}/"
}
failure {
echo "✗ Build/push/deploy failed — см. лог"
}
}
}
}
post {
success {
echo "✓ Version: ${IMAGE_VERSION}"
echo "✓ Harbor: ${HARBOR_IMAGE}:{${IMAGE_VERSION},latest}"
echo "✓ Hub: ${DOCKERHUB_IMAGE}:{${IMAGE_VERSION},latest}"
echo "✓ Live: https://${INGRESS_HOST}/"
}
failure {
echo "✗ Pipeline failed"
}
}
}
+26 -5
View File
@@ -4,19 +4,22 @@ SERVICE_SIM := simulator
PYTEST_OFFLINE := -m "not integration and not compatibility"
# Docker Hub release image (runtime target only — not the local bind-mount "dev" image).
# SemVer source of truth: VERSION (synced to pyproject + Helm via bump targets).
DOCKERHUB_USER ?= inecs
IMAGE_NAME ?= proxmox-api-simulator
VERSION ?= $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.1.0)
DOCKER_IMAGE ?= $(DOCKERHUB_USER)/$(IMAGE_NAME)
PUSH_LATEST ?= 1
COMPOSE_RELEASE ?= $(COMPOSE) -f docker-compose.release.yml
HELM_CHART ?= ./helm/proxmox-api-simulator
.PHONY: help install format lint typecheck test test-unit test-integration test-contract test-compatibility test-surface evidence coverage run dev up up-local down down-local restart logs docker-build docker-up docker-down docker-logs docker-restart db-up db-down db-migrate db-reset api-import api-diff seed clean ci ci-all shell release release-build release-up release-down release-seed helm-deps helm-template helm-lint pulumi-tests push
.PHONY: help install format lint typecheck test test-unit test-integration test-contract test-compatibility test-surface evidence coverage run dev up up-local down down-local restart logs docker-build docker-up docker-down docker-logs docker-restart db-up db-down db-migrate db-reset api-import api-diff seed clean ci ci-all shell release release-build release-up release-down release-seed helm-deps helm-template helm-lint pulumi-tests push bump-patch bump-minor version-commit
help: ## Show available commands
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_-]+:.*## / {printf "%-22s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
@echo
@echo "Version: $$(cat VERSION 2>/dev/null || echo 0.1.0) (make bump-patch | bump-minor)"
install: ## Build runtime and development images
@test -f .env || cp .env.example .env
@@ -213,16 +216,34 @@ pulumi-tests: ## Full Pulumi suite (surface majors 69 + lifecycle, HTML repor
$(MAKE) -C pulumi-tests up
$(MAKE) -C pulumi-tests test
push: ## git add ., prompt for commit message, push origin (GitHub + antropoff.ru)
bump-patch: ## VERSION +0.0.1 (pyproject + Helm)
@PYTHONPATH=. python3 scripts/bump_version.py patch
@echo "VERSION → $$(cat VERSION)"
bump-minor: ## VERSION +0.1.0 (pyproject + Helm)
@PYTHONPATH=. python3 scripts/bump_version.py minor
@echo "VERSION → $$(cat VERSION)"
version-commit: ## Stage version files and commit "Bump version to …"
@git add VERSION pyproject.toml helm/proxmox-api-simulator/Chart.yaml helm/proxmox-api-simulator/values.yaml
@if git diff --cached --quiet; then \
echo "Nothing to commit for version files."; \
else \
git commit -m "Bump version to $$(cat VERSION)."; \
fi
push: ## bump-patch + commit version + push origin (GitHub + antropoff.ru)
@$(MAKE) bump-patch
@$(MAKE) version-commit
@set -e; \
git add .; \
echo "=== staged ==="; \
git status --short; \
echo; \
if git diff --cached --quiet; then \
echo "Nothing to commit — pushing current branch."; \
echo "Nothing else to commit — pushing current branch."; \
else \
echo "Enter commit message, then Ctrl-D:"; \
echo "Enter commit message for remaining changes, then Ctrl-D:"; \
msg=$$(cat </dev/tty); \
if [ -z "$$msg" ]; then echo "Empty commit message, aborting." >&2; exit 1; fi; \
git commit -m "$$msg"; \
+1
View File
@@ -0,0 +1 @@
0.1.1
+2 -1
View File
@@ -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
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" / "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
+2
View File
@@ -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": [
{
+25
View File
@@ -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">
<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
View File
@@ -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)
+1
View File
@@ -19,6 +19,7 @@ page. Russian mirrors live under [`ru/`](ru/README.md).
| [Domains](domains/README.md) | QEMU, LXC, storage, HA, SDN, … |
| [Web UI](web-ui.md) | Interactive console and catalogs |
| [Operations](operations.md) | Migrate, reseed, upgrade, Hub publish |
| [CI/CD (Jenkins)](ci-cd.md) | VERSION, Harbor/Hub push, deploy to proxmox.devops.org.ru |
| [Docker Hub overview](docker-hub-overview.md) | Paste-ready Hub repository description |
| [Kubernetes / Helm](kubernetes.md) | Hub image + Ingress + Let's Encrypt |
| [Security](security.md) | Lab threat model and credentials |
+77
View File
@@ -0,0 +1,77 @@
**Language / Язык:** [English](ci-cd.md) | [Русский](ru/ci-cd.md)
# CI/CD (Jenkins + Gitea)
SemVer lives in [`VERSION`](../VERSION). Bump locally with `make bump-patch` /
`make bump-minor` (also syncs `pyproject.toml` and Helm chart/values). Jenkins
does **not** bump — it builds whatever is in the commit.
Public lab URL after deploy: **https://proxmox.devops.org.ru/**
## Pipeline
Single [`Jenkinsfile`](../Jenkinsfile) on `main` / `master`:
1. **Build & push** (agent `docker` / DinD) — multi-arch `linux/amd64,linux/arm64`
to Harbor and Docker Hub with tags **`:{VERSION}`** and **`:latest`** only.
2. **Deploy**`kubectl set image` on `simulators/simulators-proxmox` (main
container `simulator` + initContainer `migrate`), then rollout status.
First cluster install (Ingress, Postgres, secrets) is **not** this job — use
`make addon-simulators` in DevOpsTools/K3S (`simulators_proxmox_host:
proxmox.devops.org.ru`).
### Images
| Registry | Repository |
|---|---|
| Harbor | `hub.antropoff.ru/devops-tools/proxmox-api-simulator` |
| Docker Hub | `inecs/proxmox-api-simulator` |
### Credentials (Jenkins Global)
| ID | Type | Use |
|---|---|---|
| `harbor-devops-tools-push-pull-access` | Username/password | Harbor robot push |
| `docker-hub` | Username/password | Docker Hub `inecs` |
| `k3s-kubeconfig` | Secret file | Kubeconfig for deploy |
| `ssh-gitea-key` | SSH private key | Gitea SCM checkout |
## Gitea setup
1. Ensure the repo is on Gitea (mirror or primary) with branch `main`.
2. Deploy key / Jenkins credential `ssh-gitea-key` can clone the repo
(`git@…:…/proxmox_api_simulator.git` or your path).
3. Optional: webhook from Gitea → Jenkins Multibranch (or rely on Jenkins SCM
polling / Organization Folder).
## Jenkins setup
1. **Credentials** — create the four IDs above if missing (same as Wrapped).
2. **Agent** — Kubernetes cloud pod template with label `docker` and a
`docker` container (DinD / buildx), same as Wrapped.
3. **Job** — Multibranch Pipeline (or Pipeline from SCM):
- Script Path: `Jenkinsfile`
- Branch discover: `main` (and `master` if needed)
- SCM: Gitea SSH URL + credential `ssh-gitea-key`
4. **Harbor project**`devops-tools` must allow the robot to push
`proxmox-api-simulator` (create the repository on first push or pre-create).
5. **Docker Hub**`inecs/proxmox-api-simulator` exists / push rights for
credential `docker-hub`.
6. **Cluster** — release `simulators` in namespace `simulators` already
installed (`make addon-simulators`). Deployment name must be
`simulators-proxmox`.
7. Run once on `main` after bumping `VERSION` and pushing. Confirm:
- Harbor/Hub tags `:0.x.y` and `:latest`
- `kubectl -n simulators get deploy,ing`
- Help → About shows badge `v0.x.y` on https://proxmox.devops.org.ru/
## Local release (without Jenkins)
```bash
make bump-patch # or bump-minor
make version-commit
git push origin HEAD
# or manual Hub-only:
make release # DOCKER_IMAGE=inecs/proxmox-api-simulator
```
+1
View File
@@ -19,6 +19,7 @@
| [Домены](domains/README.md) | QEMU, LXC, storage, HA, SDN, … |
| [Web UI](web-ui.md) | Интерактивная консоль и каталоги |
| [Эксплуатация](operations.md) | Миграция, reseed, обновление, публикация в Hub |
| [CI/CD (Jenkins)](ci-cd.md) | VERSION, push Harbor/Hub, деплой на proxmox.devops.org.ru |
| [Обзор Docker Hub](../docker-hub-overview.md) | Готовый текст описания репозитория Hub (EN) |
| [Kubernetes / Helm](kubernetes.md) | Образ Hub + Ingress + Let's Encrypt |
| [Безопасность](security.md) | Модель угроз лаборатории и учётные данные |
+76
View File
@@ -0,0 +1,76 @@
**Language / Язык:** [English](../ci-cd.md) | [Русский](ci-cd.md)
# CI/CD (Jenkins + Gitea)
SemVer хранится в [`VERSION`](../../VERSION). Локальный bump:
`make bump-patch` / `make bump-minor` (синхронизирует `pyproject.toml` и Helm).
Jenkins версию **не** поднимает — собирает то, что в коммите.
Публичный URL после деплоя: **https://proxmox.devops.org.ru/**
## Pipeline
Один [`Jenkinsfile`](../../Jenkinsfile) на ветках `main` / `master`:
1. **Build & push** (agent `docker` / DinD) — multi-arch `linux/amd64,linux/arm64`
в Harbor и Docker Hub, теги только **`:{VERSION}`** и **`:latest`**.
2. **Deploy**`kubectl set image` для `simulators/simulators-proxmox`
(контейнер `simulator` + initContainer `migrate`), затем rollout status.
Первый install в кластере (Ingress, Postgres, секреты) — **не** этот job, а
`make addon-simulators` в DevOpsTools/K3S (`simulators_proxmox_host:
proxmox.devops.org.ru`).
### Образы
| Registry | Repository |
|---|---|
| Harbor | `hub.antropoff.ru/devops-tools/proxmox-api-simulator` |
| Docker Hub | `inecs/proxmox-api-simulator` |
### Credentials (Jenkins Global)
| ID | Тип | Назначение |
|---|---|---|
| `harbor-devops-tools-push-pull-access` | Username/password | Push в Harbor |
| `docker-hub` | Username/password | Docker Hub `inecs` |
| `k3s-kubeconfig` | Secret file | Kubeconfig для deploy |
| `ssh-gitea-key` | SSH private key | Checkout из Gitea |
## Что сделать в Gitea
1. Репозиторий на Gitea (зеркало или primary), ветка `main`.
2. Deploy key / credential Jenkins `ssh-gitea-key` с правом clone
(`git@…:…/proxmox_api_simulator.git`).
3. По желанию: webhook Gitea → Jenkins Multibranch (или polling / Organization
Folder).
## Что сделать в Jenkins
1. **Credentials** — четыре ID выше (как у Wrapped), если ещё нет.
2. **Agent** — pod template label `docker` с контейнером `docker` (DinD /
buildx), как у Wrapped.
3. **Job** — Multibranch Pipeline (или Pipeline from SCM):
- Script Path: `Jenkinsfile`
- Ветки: `main``master` при необходимости)
- SCM: SSH URL Gitea + `ssh-gitea-key`
4. **Harbor** — проект `devops-tools`, robot может push
`proxmox-api-simulator`.
5. **Docker Hub** — репозиторий `inecs/proxmox-api-simulator`, права у
`docker-hub`.
6. **Кластер** — релиз `simulators` в ns `simulators` уже установлен
(`make addon-simulators`). Deployment: `simulators-proxmox`.
7. После bump `VERSION` и push в `main` — прогнать job и проверить:
- теги `:0.x.y` и `:latest` в Harbor/Hub
- `kubectl -n simulators get deploy,ing`
- Help → About: badge `v0.x.y` на https://proxmox.devops.org.ru/
## Локальный релиз без Jenkins
```bash
make bump-patch # или bump-minor
make version-commit
git push origin HEAD
# или только Hub вручную:
make release
```
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: proxmox-api-simulator
description: Stateful Proxmox VE API simulator (PostgreSQL-backed) for labs and CI
type: application
version: 0.1.0
appVersion: "0.1.0"
version: 0.1.1
appVersion: "0.1.1"
home: https://github.com/sergeyantropoff/proxmox-api-simulator
keywords:
- proxmox
@@ -9,8 +9,8 @@
# -n proxmox-sim --create-namespace \
# -f helm/proxmox-api-simulator/values-ingress-example.yaml \
# --set certManager.email=you@example.com \
# --set ingress.hosts[0].host=pve-sim.example.com \
# --set ingress.tls[0].hosts[0]=pve-sim.example.com \
# --set 'ingress.hosts[0].host=proxmox.devops.org.ru' \
# --set 'ingress.tls[0].hosts[0]=proxmox.devops.org.ru' \
# --set secret.ticketSigningKey="$(openssl rand -hex 32)" \
# --set postgresql.auth.password="$(openssl rand -hex 16)"
@@ -45,14 +45,14 @@ ingress:
# custom-http-errors on this Ingress (overrides controller defaults):
nginx.ingress.kubernetes.io/custom-http-errors: "502,503"
hosts:
- host: pve-sim.example.com
- host: proxmox.devops.org.ru
paths:
- path: /
pathType: Prefix
tls:
- secretName: proxmox-api-simulator-tls
hosts:
- pve-sim.example.com
- proxmox.devops.org.ru
certManager:
enabled: true
+2 -2
View File
@@ -10,8 +10,8 @@ replicaCount: 1
image:
repository: inecs/proxmox-api-simulator
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# Synced from VERSION via make bump-patch / scripts/bump_version.py
tag: "0.1.1"
imagePullSecrets: []
nameOverride: ""
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "proxmox-api-simulator"
version = "0.1.0"
version = "0.1.1"
description = "Stateful asynchronous Proxmox VE API simulator"
readme = "README.md"
requires-python = ">=3.13,<3.14"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""CLI: bump project SemVer (VERSION + pyproject + Helm chart/values)."""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.version import bump_minor_version, bump_patch_version # noqa: E402
def main(argv: list[str] | None = None) -> int:
args = argv if argv is not None else sys.argv[1:]
if len(args) != 1 or args[0] not in {"patch", "minor"}:
print("Usage: bump_version.py patch|minor", file=sys.stderr)
return 1
version = bump_patch_version() if args[0] == "patch" else bump_minor_version()
print(version)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2 -1
View File
@@ -37,11 +37,12 @@ def _snapshot() -> Snapshot:
def test_list_majors_includes_latest_releases() -> None:
payload = list_majors(runtime_version="9.2.3")
payload = list_majors(runtime_version="9.2.3", app_version="0.1.0")
majors_list = cast(list[dict[str, Any]], payload["majors"])
majors = {item["major"] for item in majors_list}
assert majors == {6, 7, 8, 9}
assert payload["runtime_version"] == "9.2.3"
assert payload["app_version"] == "0.1.0"
def test_list_majors_includes_artifact_urls() -> None:
+14
View File
@@ -0,0 +1,14 @@
"""SemVer helpers."""
from __future__ import annotations
from app.version import get_app_version, get_app_version_label, parse_version
def test_get_app_version_reads_version_file() -> None:
version = get_app_version()
major, minor, patch = parse_version(version)
assert major >= 0
assert minor >= 0
assert patch >= 0
assert get_app_version_label() == f"v{version}"
+4 -1
View File
@@ -111,7 +111,10 @@ async def test_ui_versions_and_catalog_endpoints() -> None:
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
versions = await client.get("/ui/api/versions")
assert versions.status_code == 200
assert {item["major"] for item in versions.json()["majors"]} == {6, 7, 8, 9}
body = versions.json()
assert {item["major"] for item in body["majors"]} == {6, 7, 8, 9}
assert body["app_version"]
assert body["app_version"].count(".") == 2
catalog = await client.get("/ui/api/catalog", params={"major": 9})
assert catalog.status_code == 200
assert catalog.json()["source_version"] == "9.2.3"