feat(version): /api/version + build-time version/commit stamping

Add a VERSION file (0.1.0) and inject APP_VERSION/GIT_SHA/BUILD_DATE as Docker
build-args (both stages; Vite inlines them into the SPA). New public GET
/api/version returns app_version, git_sha, build_date and the Alembic head as the
database revision. deploy.sh and the localdev build pass the args.
This commit is contained in:
npeter83 2026-06-15 00:06:57 +02:00
parent 8a0da8df83
commit 882429d6af
10 changed files with 69 additions and 2 deletions

View file

@ -1,5 +1,15 @@
# Build/version info, injected via --build-arg (defaults keep un-built/dev runs working).
ARG APP_VERSION=dev
ARG GIT_SHA=unknown
ARG BUILD_DATE=
# Stage 1: build the frontend SPA
FROM node:20-alpine AS frontend
ARG APP_VERSION
ARG GIT_SHA
ARG BUILD_DATE
# Vite inlines VITE_*-prefixed env into the bundle at build time.
ENV VITE_APP_VERSION=$APP_VERSION VITE_GIT_SHA=$GIT_SHA VITE_BUILD_DATE=$BUILD_DATE
WORKDIR /fe
COPY frontend/package.json ./
RUN npm install
@ -8,10 +18,16 @@ RUN npm run build
# Stage 2: backend runtime, serving the built SPA
FROM python:3.12-slim
ARG APP_VERSION
ARG GIT_SHA
ARG BUILD_DATE
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
PIP_DISABLE_PIP_VERSION_CHECK=1 \
APP_VERSION=$APP_VERSION \
GIT_SHA=$GIT_SHA \
BUILD_DATE=$BUILD_DATE
WORKDIR /app

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.0

View file

@ -9,6 +9,11 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
app_name: str = "Siftlode"
# Build/version info, injected at image build time (Docker build-args -> env). Defaults
# apply for un-built/dev runs; the About dialog and version banner surface these.
app_version: str = "dev"
git_sha: str = "unknown"
build_date: str = ""
database_url: str = "postgresql+psycopg://subfeed:subfeed@db:5432/subfeed"

View file

@ -26,7 +26,7 @@ from starlette.middleware.sessions import SessionMiddleware
from app import auth
from app.config import settings
from app.routes import admin, channels, feed, health, me, quota, sync, tags
from app.routes import admin, channels, feed, health, me, quota, sync, tags, version
from app.scheduler import shutdown_scheduler, start_scheduler
@ -68,6 +68,7 @@ app.include_router(me.router)
app.include_router(channels.router)
app.include_router(admin.router)
app.include_router(quota.router)
app.include_router(version.router)
# The built SPA (populated by the Docker frontend build stage).
STATIC_DIR = Path(__file__).parent / "static_spa"

View file

@ -0,0 +1,23 @@
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.config import settings
from app.db import get_db
router = APIRouter(prefix="/api", tags=["version"])
@router.get("/version")
def get_version(db: Session = Depends(get_db)) -> dict:
"""Build + schema version, for the About dialog and the 'new version' banner.
Intentionally public (no auth) and cheap: just env-baked build info plus the current
Alembic migration head (the 'database version'). app_version/git_sha/build_date are
injected at image build time (see the Dockerfile build-args)."""
db_revision = db.execute(text("SELECT version_num FROM alembic_version")).scalar()
return {
"app_version": settings.app_version,
"git_sha": settings.git_sha,
"build_date": settings.build_date or None,
"db_revision": db_revision,
}

View file

@ -15,6 +15,11 @@ git fetch --quiet origin
git checkout --quiet main
git pull --ff-only --quiet
# Stamp the image with the version/commit being built (read by /api/version + the SPA).
export APP_VERSION="$(cat VERSION 2>/dev/null || echo dev)"
export GIT_SHA="$(git rev-parse --short HEAD)"
export BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker compose -f docker-compose.prod.yml --env-file "$ROOT/.env" up -d --build
docker image prune -f >/dev/null || true
docker compose -f docker-compose.prod.yml --env-file "$ROOT/.env" ps

View file

@ -21,6 +21,10 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-dev}
GIT_SHA: ${GIT_SHA:-unknown}
BUILD_DATE: ${BUILD_DATE:-}
env_file: .env
environment:
# Exactly one scheduler may write to the shared DB; the server owns it.

View file

@ -37,6 +37,10 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-dev}
GIT_SHA: ${GIT_SHA:-unknown}
BUILD_DATE: ${BUILD_DATE:-}
image: siftlode-api:local
env_file:
- /srv/siftlode/.env

View file

@ -41,6 +41,10 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-dev}
GIT_SHA: ${GIT_SHA:-unknown}
BUILD_DATE: ${BUILD_DATE:-}
env_file: .env
environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD:-subfeed}@db:5432/${POSTGRES_DB:-subfeed}

View file

@ -18,6 +18,10 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
APP_VERSION: ${APP_VERSION:-dev}
GIT_SHA: ${GIT_SHA:-unknown}
BUILD_DATE: ${BUILD_DATE:-}
env_file: .env
environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD:-subfeed}@db:5432/${POSTGRES_DB:-subfeed}