diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index d5902ea0..0366f156 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -50,9 +50,11 @@ jobs: if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/}" echo "args=-t ${IMAGE}:${VERSION} -t ${IMAGE}:latest" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "::notice::Release build: ${VERSION} + latest" else echo "args=-t ${IMAGE}:main" >> "$GITHUB_OUTPUT" + echo "version=main" >> "$GITHUB_OUTPUT" echo "::notice::Main-branch build: :main" fi @@ -93,4 +95,7 @@ jobs: - name: Build and push if: steps.guard.outputs.ready == 'true' - run: docker buildx build --push ${{ steps.tags.outputs.args }} . + run: | + docker buildx build \ + --build-arg MINSTREL_VERSION="${{ steps.tags.outputs.version }}" \ + --push ${{ steps.tags.outputs.args }} . diff --git a/Dockerfile b/Dockerfile index 34cc41ad..436df34c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,13 @@ COPY . . # Overwrite the committed placeholder with the freshly-built SPA assets. COPY --from=web /web/build ./web/build ENV CGO_ENABLED=0 -RUN go build -trimpath -ldflags="-s -w" -o /out/minstrel ./cmd/minstrel +# Version stamping: release.yml passes the git tag via MINSTREL_VERSION +# build-arg; local `docker build` falls back to "dev". Surfaced at +# /healthz for operator-side image-version verification. +ARG MINSTREL_VERSION=dev +RUN go build -trimpath \ + -ldflags="-s -w -X 'git.fabledsword.com/bvandeusen/minstrel/internal/server.ServerVersion=${MINSTREL_VERSION}'" \ + -o /out/minstrel ./cmd/minstrel FROM debian:bookworm-slim RUN apt-get update \ diff --git a/internal/server/server.go b/internal/server/server.go index db363e6e..699418ac 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -148,6 +148,7 @@ func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{ "status": "ok", + "version": ServerVersion, "min_client_version": MinClientVersion, }) } diff --git a/internal/server/version.go b/internal/server/version.go index b97672d0..1d87c7d4 100644 --- a/internal/server/version.go +++ b/internal/server/version.go @@ -4,3 +4,13 @@ package server // accepts. Bump it when a server change requires a paired client update; // older clients see version_too_old at /healthz and refuse to operate. const MinClientVersion = "0.1.0" + +// ServerVersion is the deployed server image's version tag. Defaults to +// "dev" for local builds; overridden at link time via: +// +// -ldflags="-X 'git.fabledsword.com/bvandeusen/minstrel/internal/server.ServerVersion=v2026.05.10.2'" +// +// release.yml passes the git tag through MINSTREL_VERSION build-arg → +// Dockerfile ldflag. Surfaced at /healthz so operators can verify which +// image their container is running without exec'ing into it. +var ServerVersion = "dev" diff --git a/web/src/lib/components/MobileAppDownload.svelte b/web/src/lib/components/MobileAppDownload.svelte index 67734517..6766e96d 100644 --- a/web/src/lib/components/MobileAppDownload.svelte +++ b/web/src/lib/components/MobileAppDownload.svelte @@ -11,6 +11,10 @@ // bandwidth abuse on the APK stream, so this component only shows // for logged-in users. Mount in Settings, never in pre-auth surfaces. + // Server returns snake_case (clientVersionResponse in + // internal/api/client_assets.go). Map to a camelCase struct for + // the rest of the component. + type WireInfo = { version: string; apk_url: string; size_bytes: number }; type Info = { version: string; apkUrl: string; sizeBytes: number }; let info = $state(null); @@ -22,12 +26,12 @@ onMount(async () => { try { - const body = await api.get>('/api/client/version'); - if (!body.version || !body.apkUrl) return; + const body = await api.get>('/api/client/version'); + if (!body.version || !body.apk_url) return; info = { version: body.version, - apkUrl: body.apkUrl, - sizeBytes: body.sizeBytes ?? 0 + apkUrl: body.apk_url, + sizeBytes: body.size_bytes ?? 0 }; } catch { // 404 (no APK), 401 (somehow unauthed), network error — silent. @@ -36,17 +40,23 @@ {#if info} - - - - Get the Android app - - {info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''} +
+

Mobile app

+

+ Install the Android app paired to this server. Sign in with the same account. +

+
+ + + Get the Android app + + {info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''} + - - + +
{/if} diff --git a/web/src/lib/components/ServerVersion.svelte b/web/src/lib/components/ServerVersion.svelte new file mode 100644 index 00000000..2fc80eea --- /dev/null +++ b/web/src/lib/components/ServerVersion.svelte @@ -0,0 +1,37 @@ + + +{#if version} +

Server {version}

+{/if} diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index 60ec8465..4dbabce3 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -18,6 +18,7 @@ import { errCode } from '$lib/api/errors'; import { pushToast } from '$lib/stores/toast.svelte'; import MobileAppDownload from '$lib/components/MobileAppDownload.svelte'; + import ServerVersion from '$lib/components/ServerVersion.svelte'; const queryClient = useQueryClient(); @@ -325,11 +326,9 @@ -
-

Mobile app

-

- Install the Android app paired to this server. Sign in with the same account. -

- -
+ + +
+ +