diff --git a/.dockerignore b/.dockerignore index 8d40d2b..e45387c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,7 +23,7 @@ __pycache__ .pytest_cache # Node modules (installed fresh in build) -node_modules +**/node_modules # IDE and OS files .vscode diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 79fb122..7c9bb1a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -25,6 +25,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Get short commit hash + id: commit + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + - name: Build and push Docker image uses: docker/build-push-action@v6 with: @@ -33,5 +37,9 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: speedarr/speedarr:develop + build-args: | + SPEEDARR_VERSION=develop + SPEEDARR_COMMIT=${{ steps.commit.outputs.short_sha }} + SPEEDARR_BRANCH=develop cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e26a343..fe3ed7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Extract metadata id: meta uses: docker/metadata-action@v5 @@ -45,6 +49,10 @@ jobs: platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + SPEEDARR_VERSION=${{ steps.version.outputs.version }} + SPEEDARR_COMMIT=${{ github.sha }} + SPEEDARR_BRANCH=main cache-from: type=gha cache-to: type=gha,mode=max diff --git a/backend/Dockerfile b/backend/Dockerfile index c8b3d41..1c1660e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,23 @@ # Speedarr Single-Container Dockerfile with Frontend +# Build args (declared globally, re-declared per stage as needed) +ARG SPEEDARR_VERSION=dev +ARG SPEEDARR_COMMIT=unknown +ARG SPEEDARR_BRANCH=unknown + # Stage 1: Build React Frontend FROM node:20.11-alpine3.19 AS frontend-builder +# Re-declare ARGs for this stage +ARG SPEEDARR_VERSION +ARG SPEEDARR_COMMIT +ARG SPEEDARR_BRANCH + +# Set as env vars for Vite define (read at build time) +ENV VITE_APP_VERSION=${SPEEDARR_VERSION} +ENV VITE_APP_COMMIT=${SPEEDARR_COMMIT} +ENV VITE_APP_BRANCH=${SPEEDARR_BRANCH} + WORKDIR /frontend # Copy package files @@ -20,6 +35,16 @@ RUN npm run build # Stage 2: Python Backend + Serve Frontend FROM python:3.11.7-slim-bookworm AS base +# Re-declare ARGs for this stage +ARG SPEEDARR_VERSION +ARG SPEEDARR_COMMIT +ARG SPEEDARR_BRANCH + +# Set as env vars for backend runtime (os.getenv) +ENV SPEEDARR_VERSION=${SPEEDARR_VERSION} +ENV SPEEDARR_COMMIT=${SPEEDARR_COMMIT} +ENV SPEEDARR_BRANCH=${SPEEDARR_BRANCH} + # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 3cddd4e..7d31172 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -2,4 +2,8 @@ Speedarr - Intelligent bandwidth management for Plex and download clients. """ -__version__ = "0.1.0" +import os + +__version__ = os.getenv("SPEEDARR_VERSION", "dev") +__commit__ = os.getenv("SPEEDARR_COMMIT", "unknown") +__branch__ = os.getenv("SPEEDARR_BRANCH", "unknown") diff --git a/backend/app/config.py b/backend/app/config.py index 61beee4..666f68f 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -350,7 +350,7 @@ class Settings(BaseSettings): # Application app_name: str = "Speedarr" - app_version: str = "0.1.0" + app_version: str = Field(default_factory=lambda: __import__('app').__version__) debug: bool = False # Server diff --git a/backend/app/main.py b/backend/app/main.py index 33d7847..4e5f36c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -11,6 +11,7 @@ from fastapi.responses import FileResponse from loguru import logger +from app import __version__, __commit__, __branch__ from app.config import settings, SpeedarrConfig from app.middleware.correlation import CorrelationIdMiddleware from app.constants import ( @@ -296,7 +297,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="Speedarr", description="Intelligent bandwidth management for Plex and download clients", - version="0.1.0", + version=__version__, lifespan=lifespan ) @@ -332,7 +333,9 @@ async def health_check(): """Combined health check endpoint (no auth required).""" return { "status": "healthy", - "version": "0.1.0" + "version": __version__, + "commit": __commit__, + "branch": __branch__, } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index f6ac66e..404c19e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -189,7 +189,7 @@ class ApiClient { return this.deduplicatedGet('/status/current'); } - async getHealth(): Promise<{ status: string; version: string }> { + async getHealth(): Promise<{ status: string; version: string; commit: string; branch: string }> { const response = await this.client.get('/status/health'); return response.data; } diff --git a/frontend/src/components/settings/SystemSettings.tsx b/frontend/src/components/settings/SystemSettings.tsx index bbca9e1..884a7e8 100644 --- a/frontend/src/components/settings/SystemSettings.tsx +++ b/frontend/src/components/settings/SystemSettings.tsx @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Loader2, AlertCircle, CheckCircle, Download } from 'lucide-react'; +import { Loader2, AlertCircle, CheckCircle, Download, Info } from 'lucide-react'; import { Select, SelectContent, @@ -117,6 +117,10 @@ export const SystemSettings: React.FC = () => { } }; + const versionDisplay = __APP_VERSION__ === 'develop' + ? `develop (${__APP_COMMIT__})` + : __APP_VERSION__; + if (isLoading) { return ( @@ -141,104 +145,125 @@ export const SystemSettings: React.FC = () => { } return ( - - - System Configuration - - Core system settings and behavior - - - - {error && ( - - - {error} - - )} + <> + + + System Configuration + + Core system settings and behavior + + + + {error && ( + + + {error} + + )} + + {success && ( + + + {success} + + )} + +
+
+ + updateConfig('update_frequency', parseInt(e.target.value))} + placeholder="5" + disabled={isSaving} + /> +

+ How often to poll Plex, download clients, and SNMP (5-300 seconds, default: 5) +

+
+ +
+ + +

+ Logging verbosity level (default: INFO) +

+
+ +
+ +
+ +
+

+ Download application logs with sensitive data (passwords, API keys) redacted +

+
- {success && ( - - - {success} - - )} - -
-
- - updateConfig('update_frequency', parseInt(e.target.value))} - placeholder="5" - disabled={isSaving} - /> -

- How often to poll Plex, download clients, and SNMP (5-300 seconds, default: 5) -

-
- - -

- Logging verbosity level (default: INFO) -

+ {isSaving && } + Save Changes +
+ + -
- -
- -
-

- Download application logs with sensitive data (passwords, API keys) redacted -

+ + + + + About + + + +
+ Version + {versionDisplay} + Commit + {__APP_COMMIT__} + Branch + {__APP_BRANCH__}
- -
- -
- -
- - + + + ); }; diff --git a/frontend/src/globals.d.ts b/frontend/src/globals.d.ts new file mode 100644 index 0000000..6d941e5 --- /dev/null +++ b/frontend/src/globals.d.ts @@ -0,0 +1,3 @@ +declare const __APP_VERSION__: string; +declare const __APP_COMMIT__: string; +declare const __APP_BRANCH__: string; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b9c1b67..15241c5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,11 @@ import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + define: { + __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || 'dev'), + __APP_COMMIT__: JSON.stringify(process.env.VITE_APP_COMMIT || 'unknown'), + __APP_BRANCH__: JSON.stringify(process.env.VITE_APP_BRANCH || 'unknown'), + }, resolve: { alias: { '@': path.resolve(__dirname, './src'),