diff --git a/Dockerfile b/Dockerfile index 6f98c9b..806b8c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/backend/app/config.py b/backend/app/config.py index 808dcf6..0f796ab 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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" diff --git a/backend/app/main.py b/backend/app/main.py index 7b1eca8..cb07a68 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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" diff --git a/backend/app/routes/version.py b/backend/app/routes/version.py new file mode 100644 index 0000000..ccaa87a --- /dev/null +++ b/backend/app/routes/version.py @@ -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, + } diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 7918555..5f4f341 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -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 diff --git a/docker-compose.localdev.yml b/docker-compose.localdev.yml index 9835770..1ca87f1 100644 --- a/docker-compose.localdev.yml +++ b/docker-compose.localdev.yml @@ -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. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 215d2ff..a258ac3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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 diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 2da614d..cdc3624 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -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} diff --git a/docker-compose.yml b/docker-compose.yml index 5ead8e5..cf9c976 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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}