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) <noreply@anthropic.com>
This commit is contained in:
AdrianoandClaude Opus 4.8 committed 2026-06-03 14:40:52 +02:00
1 parent a88a064aa8
commit cf726b479e
8 files changed
+126 -1

No files matched your search

+8 -1
View File
@@ -12,13 +12,20 @@ COPY pyproject.toml README.md ./
COPY src/ src/ COPY src/ src/
RUN uv sync --no-dev RUN uv sync --no-dev
COPY config.yaml ./ COPY config.yaml VERSION ./
COPY web/ web/ COPY web/ web/
ENV PRESA_MANO_CONFIG=/app/config.yaml ENV PRESA_MANO_CONFIG=/app/config.yaml
ENV PRESA_MANO_DATA=/app/data ENV PRESA_MANO_DATA=/app/data
ENV PRESA_MANO_PORT=8890 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 EXPOSE 8890
# Shell form so $PRESA_MANO_PORT viene espanso a runtime: cambi la porta in # Shell form so $PRESA_MANO_PORT viene espanso a runtime: cambi la porta in
+1
View File
@@ -0,0 +1 @@
1.0.0
Executable
+40
View File
@@ -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})"
+6
View File
@@ -25,6 +25,7 @@ from .api import (
) )
from .clients import G1StatusError from .clients import G1StatusError
from .config import load_config from .config import load_config
from .version import build_info
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
@@ -102,6 +103,11 @@ def health() -> dict:
return {"status": "ok"} return {"status": "ok"}
@app.get("/api/version", tags=["meta"])
def version() -> dict:
return build_info()
@app.get("/", include_in_schema=False) @app.get("/", include_in_schema=False)
def index() -> HTMLResponse: def index() -> HTMLResponse:
idx = WEB_DIR / "index.html" idx = WEB_DIR / "index.html"
+35
View File
@@ -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}
+25
View File
@@ -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)"
+10
View File
@@ -742,6 +742,16 @@ async function refreshHomeRobotSelector() {
refreshHomeRobotSelector(); refreshHomeRobotSelector();
startHomePoll(); 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 ---------- // ---------- init ----------
(async () => { (async () => {
try { try {
+1
View File
@@ -20,6 +20,7 @@
</nav> </nav>
<div class="status-strip" id="statusStrip"></div> <div class="status-strip" id="statusStrip"></div>
<span class="active-robot-pill" id="activeRobotPill" title="Robot attualmente collegato">robot: —</span> <span class="active-robot-pill" id="activeRobotPill" title="Robot attualmente collegato">robot: —</span>
<span class="active-robot-pill" id="appVersionPill" title="versione build">V—</span>
</header> </header>
<main> <main>