diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8988eda..774023b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,7 +2,8 @@ name: Docker Build & Push on: push: - branches: [master] + branches: ["**"] + tags: ["v*"] pull_request: branches: [master] @@ -25,7 +26,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -40,13 +41,18 @@ jobs: tags: | type=sha type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Build and push uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + push: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + APP_VERSION=${{ steps.meta.outputs.version }} + APP_COMMIT=${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index e1f8fa9..88850b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,12 @@ COPY --from=frontend-build /app/frontend/dist /app/static # Persistent data directory for SQLite DB and configuration ENV DATA_DIR=/app/data ENV REDIS_URL="" + +# Version injected at build time via --build-arg +ARG APP_VERSION=dev +ARG APP_COMMIT=unknown +ENV APP_VERSION=${APP_VERSION} +ENV APP_COMMIT=${APP_COMMIT} VOLUME ["/app/data"] EXPOSE 8000 diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index b5a6486..d88f5bc 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -15,6 +15,7 @@ from app.settings import get_all_settings, get_setting, set_setting from app.cache import is_cache_available, flush_cache, reconnect_redis from app.history import get_history, delete_history_entry, clear_history from app import stats as _stats +from app.version import get_version_info router = APIRouter() @@ -690,3 +691,10 @@ async def api_record_click(body: ClickEventRequest, request: Request): ) async def api_get_stats(days: int = Query(7, ge=1, le=365)): return _stats.get_summary(days) + + +# --- Version --- + +@router.get("/version", summary="App version", tags=["System"]) +async def api_version(): + return get_version_info() diff --git a/backend/app/version.py b/backend/app/version.py new file mode 100644 index 0000000..8753455 --- /dev/null +++ b/backend/app/version.py @@ -0,0 +1,27 @@ +"""Version detection — reads env vars injected at Docker build time, falls back to git for local dev.""" + +from __future__ import annotations + +import os +import subprocess + + +def _git(cmd: list[str]) -> str: + try: + return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, cwd=os.path.dirname(__file__)).decode().strip() + except Exception: + return "" + + +def get_version_info() -> dict[str, str]: + """Return {"version": ..., "commit": ..., "local": bool_as_str}.""" + version = os.environ.get("APP_VERSION", "") + commit = os.environ.get("APP_COMMIT", "") + + if not version and not commit: + # Local dev — derive from git + version = _git(["git", "describe", "--tags", "--abbrev=0"]) or "local" + commit = _git(["git", "rev-parse", "--short", "HEAD"]) or "unknown" + return {"version": version, "commit": commit, "local": "true"} + + return {"version": version, "commit": commit[:7] if commit else "", "local": "false"} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa0fb03..8168d24 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,7 +11,7 @@ import { Bookmarks } from "@/components/Bookmarks"; import { AppHeader } from "@/components/AppHeader"; import { StatsPage } from "@/components/StatsPage"; import { History } from "@/components/History"; -import { search as apiSearch, isImageResult, getBackground, refreshBackground, getBookmarkedUrls, addBookmark, removeBookmarkByUrl, type SearchResponse, type WebResult, type ImageResult, type BackgroundInfo } from "@/lib/api"; +import { search as apiSearch, isImageResult, getBackground, refreshBackground, getBookmarkedUrls, addBookmark, removeBookmarkByUrl, getVersion, type SearchResponse, type WebResult, type ImageResult, type BackgroundInfo, type VersionInfo } from "@/lib/api"; import { cn } from "@/lib/utils"; type Category = "web" | "images"; @@ -82,10 +82,14 @@ function App() { // Bookmarked URLs for toggle state const [bookmarkedUrls, setBookmarkedUrls] = useState>(new Set()); - // Fetch background and bookmarked URLs on mount + // App version + const [versionInfo, setVersionInfo] = useState(null); + + // Fetch background, bookmarked URLs, and version on mount useEffect(() => { getBackground().then(setBgInfo).catch(() => {}); getBookmarkedUrls().then(setBookmarkedUrls).catch(() => {}); + getVersion().then(setVersionInfo).catch(() => {}); }, []); const bgUrl = bgInfo?.enabled && bgInfo?.url ? bgInfo.url : null; @@ -704,7 +708,16 @@ function App() { {/* Footer */}
-
Hey Search
+
+ Hey Search + {versionInfo && ( + + {versionInfo.local === "true" + ? `local · ${versionInfo.commit}` + : versionInfo.version} + + )} +
{/* Error toasts */} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 19a1dfc..02e3d53 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -359,3 +359,17 @@ export async function deleteHistoryEntry(id: number): Promise { export async function clearHistory(): Promise { await fetch(`${API_BASE}/history`, { method: "DELETE" }); } + +// --- Version --- + +export interface VersionInfo { + version: string; + commit: string; + local: string; // "true" | "false" +} + +export async function getVersion(): Promise { + const resp = await fetch(`${API_BASE}/version`); + if (!resp.ok) throw new Error("Failed to fetch version"); + return resp.json(); +}