diff --git a/docker-compose.yml b/docker-compose.yml index da50956..e08957b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,6 +82,9 @@ services: build: context: ./scraper dockerfile: Dockerfile + args: + VERSION: "${GIT_TAG:-dev}" + COMMIT: "${GIT_COMMIT:-unknown}" #container_name: libnovel-scraper restart: unless-stopped depends_on: @@ -131,6 +134,9 @@ services: build: context: ./ui dockerfile: Dockerfile + args: + BUILD_VERSION: "${GIT_TAG:-dev}" + BUILD_COMMIT: "${GIT_COMMIT:-unknown}" # container_name: libnovel-ui restart: unless-stopped depends_on: diff --git a/scraper/Dockerfile b/scraper/Dockerfile index 561c518..c596bd4 100644 --- a/scraper/Dockerfile +++ b/scraper/Dockerfile @@ -9,8 +9,14 @@ RUN go mod download COPY . . +# Build-time version info — injected by docker-compose or CI via --build-arg. +ARG VERSION=dev +ARG COMMIT=unknown + RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -ldflags="-s -w" -o /scraper ./cmd/scraper + go build \ + -ldflags="-s -w -X main.Version=${VERSION} -X main.Commit=${COMMIT}" \ + -o /scraper ./cmd/scraper # ── Runtime stage ────────────────────────────────────────────────────────────── FROM alpine:3.21 diff --git a/scraper/cmd/scraper/main.go b/scraper/cmd/scraper/main.go index 9216878..090e22f 100644 --- a/scraper/cmd/scraper/main.go +++ b/scraper/cmd/scraper/main.go @@ -47,6 +47,13 @@ import ( "github.com/libnovel/scraper/internal/storage" ) +// Build-time version info — injected via -ldflags during docker build. +// Falls back to "dev" / "unknown" when built without -ldflags (local dev). +var ( + Version = "dev" + Commit = "unknown" +) + func main() { logLevel := slog.LevelInfo if v := os.Getenv("LOG_LEVEL"); v != "" { @@ -183,7 +190,7 @@ func run(log *slog.Logger) error { "pocketbase_url", pbCfg.BaseURL, "pocketbase_email", pbCfg.AdminEmail, ) - srv := server.New(addr, oCfg, nf, log, store, kokoroURL, kokoroVoice) + srv := server.New(addr, oCfg, nf, log, store, kokoroURL, kokoroVoice, Version, Commit) return srv.ListenAndServe(ctx) case "save-browse": diff --git a/scraper/internal/server/server.go b/scraper/internal/server/server.go index 24f38bb..878c9e5 100644 --- a/scraper/internal/server/server.go +++ b/scraper/internal/server/server.go @@ -43,6 +43,8 @@ type Server struct { running bool kokoroURL string // Kokoro-FastAPI base URL, e.g. http://kokoro:8880 kokoroVoice string // default voice, e.g. af_bella + version string // semver tag, e.g. "v1.2.3" (set via ldflags) + commit string // short git SHA (set via ldflags) // voiceMu guards cachedVoices. voiceMu sync.RWMutex @@ -74,7 +76,7 @@ type browseCacheEntry struct { } // New creates a new Server. -func New(addr string, oCfg orchestrator.Config, novel scraper.NovelScraper, log *slog.Logger, store storage.Store, kokoroURL, kokoroVoice string) *Server { +func New(addr string, oCfg orchestrator.Config, novel scraper.NovelScraper, log *slog.Logger, store storage.Store, kokoroURL, kokoroVoice, version, commit string) *Server { return &Server{ addr: addr, oCfg: oCfg, @@ -83,6 +85,8 @@ func New(addr string, oCfg orchestrator.Config, novel scraper.NovelScraper, log store: store, kokoroURL: kokoroURL, kokoroVoice: kokoroVoice, + version: version, + commit: commit, audioJobIDs: make(map[string]string), browseInFlight: make(map[string]struct{}), browseMemCache: make(map[string]browseCacheEntry), @@ -138,6 +142,7 @@ func (s *Server) voices() []string { func (s *Server) ListenAndServe(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.handleHealth) + mux.HandleFunc("GET /api/version", s.handleVersion) mux.HandleFunc("POST /scrape", s.handleScrapeCatalogue) mux.HandleFunc("POST /scrape/book", s.handleScrapeBook) mux.HandleFunc("POST /scrape/book/range", s.handleScrapeBookRange) @@ -222,7 +227,19 @@ func (s *Server) ListenAndServe(ctx context.Context) error { func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "ok", + "version": s.version, + "commit": s.commit, + }) +} + +func (s *Server) handleVersion(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "version": s.version, + "commit": s.commit, + }) } // ─── Session cookie helpers ─────────────────────────────────────────────────── diff --git a/ui/Dockerfile b/ui/Dockerfile index fd24108..c5bfba9 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -5,6 +5,15 @@ COPY package.json package-lock.json ./ RUN npm ci COPY . . + +# Build-time version info — injected by docker-compose or CI via --build-arg. +ARG BUILD_VERSION=dev +ARG BUILD_COMMIT=unknown + +# Expose as PUBLIC_ env vars so SvelteKit's $env/dynamic/public can read them. +ENV PUBLIC_BUILD_VERSION=$BUILD_VERSION +ENV PUBLIC_BUILD_COMMIT=$BUILD_COMMIT + RUN npm run build # ── Runtime image ────────────────────────────────────────────────────────────── diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index ce3a278..b4e93cc 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import type { Snippet } from 'svelte'; import type { LayoutData } from './$types'; import { audioStore } from '$lib/audio.svelte'; + import { env } from '$env/dynamic/public'; let { children, data }: { children: Snippet; data: LayoutData } = $props(); @@ -395,6 +396,9 @@ Privacy DMCA © {new Date().getFullYear()} libnovel + {#if env.PUBLIC_BUILD_VERSION && env.PUBLIC_BUILD_VERSION !== 'dev'} + {env.PUBLIC_BUILD_VERSION}+{env.PUBLIC_BUILD_COMMIT?.slice(0, 7)} + {/if}