From cf726b479e194ac632e59d9d4e9f335b675e4fc1 Mon Sep 17 00:00:00 2001 From: Adriano Date: Wed, 3 Jun 2026 14:40:52 +0200 Subject: [PATCH] feat: badge versione build in GUI + build.sh con auto-bump - VERSION (1.0.0) + build.sh: bump patch/minor/major e deploy via docker compose - Dockerfile: ARG/ENV APP_VERSION + BUILD_TIME (iniettati come build-arg) - version.build_info(): env in Docker, fallback file VERSION + '(local)' in dev - GET /api/version; badge #appVersionPill nell'header (tooltip = build time) - test_version.py: env vs fallback Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 9 ++++++++- VERSION | 1 + build.sh | 40 +++++++++++++++++++++++++++++++++++++++ src/presa_mano/main.py | 6 ++++++ src/presa_mano/version.py | 35 ++++++++++++++++++++++++++++++++++ tests/test_version.py | 25 ++++++++++++++++++++++++ web/app.js | 10 ++++++++++ web/index.html | 1 + 8 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 VERSION create mode 100755 build.sh create mode 100644 src/presa_mano/version.py create mode 100644 tests/test_version.py diff --git a/Dockerfile b/Dockerfile index 1c9a048..9a4afff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,13 +12,20 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN uv sync --no-dev -COPY config.yaml ./ +COPY config.yaml VERSION ./ COPY web/ web/ ENV PRESA_MANO_CONFIG=/app/config.yaml ENV PRESA_MANO_DATA=/app/data ENV PRESA_MANO_PORT=8890 +# Versione build: iniettata da build.sh come --build-arg. Cambia ad ogni build +# (il valore diverso invalida solo questi due layer finali, non le dipendenze). +ARG APP_VERSION=dev +ARG BUILD_TIME=unknown +ENV APP_VERSION=${APP_VERSION} +ENV BUILD_TIME=${BUILD_TIME} + EXPOSE 8890 # Shell form so $PRESA_MANO_PORT viene espanso a runtime: cambi la porta in diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2b50e7f --- /dev/null +++ b/build.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Build + deploy di presa-mano con auto-bump della versione. +# +# ./build.sh → patch++ (1.0.1 -> 1.0.2) +# ./build.sh minor → minor++ (1.0.2 -> 1.1.0) +# ./build.sh major → major++ (1.1.0 -> 2.0.0) +# +# La nuova versione + l'ora di build vengono iniettate nell'immagine come +# build-arg e mostrate nel badge in GUI. Il file VERSION viene riscritto ma NON +# committato automaticamente (lo committi tu con le altre modifiche). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +COMPOSE_DIR="$(dirname "$SCRIPT_DIR")" # docker-compose.yml sta un livello sopra +VERSION_FILE="$SCRIPT_DIR/VERSION" + +[ -f "$VERSION_FILE" ] || echo "1.0.0" > "$VERSION_FILE" +cur="$(tr -d '[:space:]' < "$VERSION_FILE")" +IFS='.' read -r major minor patch <<< "$cur" +major="${major:-1}"; minor="${minor:-0}"; patch="${patch:-0}" + +case "${1:-patch}" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + *) echo "uso: $0 [major|minor|patch]" >&2; exit 1 ;; +esac + +new="${major}.${minor}.${patch}" +echo "$new" > "$VERSION_FILE" +build_time="$(date -u +"%Y-%m-%d %H:%M UTC")" + +echo "→ build V${new} (${build_time})" +cd "$COMPOSE_DIR" +docker compose build \ + --build-arg APP_VERSION="$new" \ + --build-arg BUILD_TIME="$build_time" \ + presa-mano +docker compose up -d presa-mano +echo "✓ deployed V${new} (${build_time})" diff --git a/src/presa_mano/main.py b/src/presa_mano/main.py index 7b3f455..68af2ed 100644 --- a/src/presa_mano/main.py +++ b/src/presa_mano/main.py @@ -25,6 +25,7 @@ from .api import ( ) from .clients import G1StatusError from .config import load_config +from .version import build_info logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") @@ -102,6 +103,11 @@ def health() -> dict: return {"status": "ok"} +@app.get("/api/version", tags=["meta"]) +def version() -> dict: + return build_info() + + @app.get("/", include_in_schema=False) def index() -> HTMLResponse: idx = WEB_DIR / "index.html" diff --git a/src/presa_mano/version.py b/src/presa_mano/version.py new file mode 100644 index 0000000..50afe90 --- /dev/null +++ b/src/presa_mano/version.py @@ -0,0 +1,35 @@ +"""Versione build dell'app, mostrata nel badge in GUI. + +In Docker i valori arrivano da env (`APP_VERSION`, `BUILD_TIME`) iniettati come +build-arg da `build.sh`. In dev locale (`uv run`) gli env non ci sono: si +ripiega sul contenuto del file `VERSION` nella root del repo + suffisso +`(local)`. +""" +from __future__ import annotations + +import os +from pathlib import Path + +_VERSION_FILE = Path(__file__).resolve().parents[2] / "VERSION" + + +def _file_version() -> str: + try: + return _VERSION_FILE.read_text(encoding="utf-8").strip() or "0.0.0" + except OSError: + return "0.0.0" + + +def build_info() -> dict: + """Ritorna {'version', 'build_time'}. + + Preferisce gli env settati al build dell'immagine; in loro assenza ripiega + sul file VERSION marcando la versione come `(local)`. + """ + version = os.environ.get("APP_VERSION") + build_time = os.environ.get("BUILD_TIME") + if not version: + version = f"{_file_version()} (local)" + if not build_time: + build_time = "(local)" + return {"version": version, "build_time": build_time} diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..e34205f --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,25 @@ +"""Unit test per build_info(): env (Docker) vs fallback VERSION file (local).""" +from __future__ import annotations + +from pathlib import Path + +from presa_mano.version import build_info + +_VERSION_FILE = Path(__file__).resolve().parents[1] / "VERSION" + + +def test_build_info_prefers_env(monkeypatch): + monkeypatch.setenv("APP_VERSION", "1.2.3") + monkeypatch.setenv("BUILD_TIME", "2026-06-03 14:22 UTC") + info = build_info() + assert info == {"version": "1.2.3", "build_time": "2026-06-03 14:22 UTC"} + + +def test_build_info_falls_back_to_version_file(monkeypatch): + monkeypatch.delenv("APP_VERSION", raising=False) + monkeypatch.delenv("BUILD_TIME", raising=False) + info = build_info() + file_ver = _VERSION_FILE.read_text(encoding="utf-8").strip() + assert file_ver in info["version"] + assert info["version"].endswith("(local)") + assert info["build_time"] == "(local)" diff --git a/web/app.js b/web/app.js index f68cc10..1e4f15f 100644 --- a/web/app.js +++ b/web/app.js @@ -742,6 +742,16 @@ async function refreshHomeRobotSelector() { refreshHomeRobotSelector(); startHomePoll(); +// Badge versione build (header). Si aggiorna ad ogni rebuild via build.sh. +async function refreshAppVersion() { + try { + const r = await api('GET', '/api/version'); + const pill = $('appVersionPill'); + if (pill) { pill.textContent = `V${r.version}`; pill.title = `build ${r.build_time}`; } + } catch {} +} +refreshAppVersion(); + // ---------- init ---------- (async () => { try { diff --git a/web/index.html b/web/index.html index ddd91dd..36efdc0 100644 --- a/web/index.html +++ b/web/index.html @@ -20,6 +20,7 @@
robot: — + V—