feat: show live build version in web UI
Build & Push Docker Image / build (push) Successful in 2m36s
Build & Push Docker Image / build (push) Successful in 2m36s
- /api/version returns version, commitSha, buildDate, trashRetentionDays - CI passes COMMIT_SHA (full), BUILD_DATE and VERSION (git tag) as docker build args; Dockerfile bakes them as env - sidebar footer shows version/sha linked to the Gitea commit - admin System tab shows version, commit link, build date, trash retention
This commit is contained in:
@@ -27,8 +27,14 @@ jobs:
|
||||
- name: Build and push
|
||||
run: |
|
||||
SHA=$(git rev-parse --short HEAD)
|
||||
FULL_SHA=$(git rev-parse HEAD)
|
||||
IMAGE="git.kubebase.de/${{ gitea.repository }}"
|
||||
docker build --no-cache -t "${IMAGE}:sha-${SHA}" -t "${IMAGE}:latest" .
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then VERSION="${{ gitea.ref_name }}"; else VERSION=""; fi
|
||||
docker build --no-cache \
|
||||
--build-arg COMMIT_SHA="$FULL_SHA" \
|
||||
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
--build-arg VERSION="$VERSION" \
|
||||
-t "${IMAGE}:sha-${SHA}" -t "${IMAGE}:latest" .
|
||||
docker push "${IMAGE}:sha-${SHA}"
|
||||
docker push "${IMAGE}:latest"
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ COPY --from=builder /app/artifacts/toolrate/dist artifacts/toolrate/dist
|
||||
ENV NODE_ENV=production
|
||||
ENV STATIC_DIR=/app/artifacts/toolrate/dist/public
|
||||
|
||||
ARG COMMIT_SHA=""
|
||||
ARG BUILD_DATE=""
|
||||
ARG VERSION=""
|
||||
ENV COMMIT_SHA=${COMMIT_SHA}
|
||||
ENV BUILD_DATE=${BUILD_DATE}
|
||||
ENV VERSION=${VERSION}
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["sh", "-c", "pnpm --filter @workspace/db run push-force && node --enable-source-maps artifacts/api-server/dist/index.mjs"]
|
||||
|
||||
@@ -8,4 +8,14 @@ router.get("/healthz", (_req, res) => {
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
router.get("/version", (_req, res) => {
|
||||
const retention = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0");
|
||||
res.json({
|
||||
version: process.env["VERSION"] || "dev",
|
||||
commitSha: process.env["COMMIT_SHA"] || null,
|
||||
buildDate: process.env["BUILD_DATE"] || null,
|
||||
trashRetentionDays: Number.isFinite(retention) ? retention : 0,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, User, ShieldCheck, AlertTriangle, Trash2 } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
@@ -8,6 +9,9 @@ import { ThemeToggle } from "@/components/theme-toggle";
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [location] = useLocation();
|
||||
const { user, isLoading, isAuthenticated, isAdmin, isLocalMode, tier, hasFeature, login, logout } = useAuth();
|
||||
const { data: version } = useGetVersion({
|
||||
query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false },
|
||||
});
|
||||
|
||||
const links = [
|
||||
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||
@@ -95,6 +99,24 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-4 py-2.5 border-t flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span className="font-medium">toolr</span>
|
||||
{version?.commitSha ? (
|
||||
<a
|
||||
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${version.commitSha}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-mono hover:underline"
|
||||
title={version.commitSha}
|
||||
>
|
||||
{version.version && version.version !== "dev"
|
||||
? version.version
|
||||
: `sha-${version.commitSha.slice(0, 7)}`}
|
||||
</a>
|
||||
) : version?.version && version.version !== "dev" ? (
|
||||
<span className="font-mono">{version.version}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
useUpdateUser,
|
||||
useDeleteUser,
|
||||
useListAuditLogs,
|
||||
useGetVersion,
|
||||
getListUsersQueryKey,
|
||||
getListAuditLogsQueryKey,
|
||||
getGetVersionQueryKey,
|
||||
} from "@workspace/api-client-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -22,7 +24,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench } from "lucide-react";
|
||||
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench, Server } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { AdminToolsTab } from "@/components/admin-tools-tab";
|
||||
|
||||
@@ -49,6 +51,9 @@ export default function Admin() {
|
||||
{ limit: 100 },
|
||||
{ query: { queryKey: getListAuditLogsQueryKey({ limit: 100 }), enabled: isAdmin } },
|
||||
);
|
||||
const { data: versionInfo } = useGetVersion({
|
||||
query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false, enabled: isAdmin },
|
||||
});
|
||||
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
@@ -154,6 +159,9 @@ export default function Admin() {
|
||||
<TabsTrigger value="audit" className="gap-2">
|
||||
<ScrollText className="w-4 h-4" /> Audit Log
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="system" className="gap-2">
|
||||
<Server className="w-4 h-4" /> System
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
@@ -274,6 +282,53 @@ export default function Admin() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="system">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>System</CardTitle>
|
||||
<CardDescription>Build information of the currently live deployment.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="divide-y">
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Version</span>
|
||||
<span className="text-sm font-medium">{versionInfo?.version || "dev"}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Commit</span>
|
||||
{versionInfo?.commitSha ? (
|
||||
<a
|
||||
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${versionInfo.commitSha}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm font-mono text-primary hover:underline"
|
||||
title={versionInfo.commitSha}
|
||||
>
|
||||
{versionInfo.commitSha.slice(0, 7)}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Build date</span>
|
||||
<span className="text-sm">
|
||||
{versionInfo?.buildDate ? format(new Date(versionInfo.buildDate), "dd.MM.yyyy HH:mm") : "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Trash retention</span>
|
||||
<span className="text-sm">
|
||||
{(versionInfo?.trashRetentionDays ?? 0) > 0
|
||||
? `${versionInfo?.trashRetentionDays} days`
|
||||
: "Keep forever"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
/** @nullable */
|
||||
commitSha?: string | null;
|
||||
/** @nullable */
|
||||
buildDate?: string | null;
|
||||
trashRetentionDays?: number;
|
||||
}
|
||||
|
||||
export type AuthModeMode = typeof AuthModeMode[keyof typeof AuthModeMode];
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ import type {
|
||||
TrashToolsInput,
|
||||
User,
|
||||
UserCreateInput,
|
||||
UserRoleUpdate
|
||||
UserRoleUpdate,
|
||||
VersionInfo
|
||||
} from './api.schemas';
|
||||
|
||||
import { customFetch } from '../custom-fetch';
|
||||
@@ -140,6 +141,84 @@ export function useHealthCheck<TData = Awaited<ReturnType<typeof healthCheck>>,
|
||||
|
||||
|
||||
|
||||
export const getGetVersionUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/version`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the running build version, commit SHA and build date
|
||||
* @summary Build version information
|
||||
*/
|
||||
export const getVersion = async ( options?: RequestInit): Promise<VersionInfo> => {
|
||||
|
||||
return customFetch<VersionInfo>(getGetVersionUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetVersionQueryKey = () => {
|
||||
return [
|
||||
`/api/version`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetVersionQueryOptions = <TData = Awaited<ReturnType<typeof getVersion>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getVersion>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetVersionQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getVersion>>> = ({ signal }) => getVersion({ signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getVersion>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type GetVersionQueryResult = NonNullable<Awaited<ReturnType<typeof getVersion>>>
|
||||
export type GetVersionQueryError = ErrorType<unknown>
|
||||
|
||||
|
||||
/**
|
||||
* @summary Build version information
|
||||
*/
|
||||
|
||||
export function useGetVersion<TData = Awaited<ReturnType<typeof getVersion>>, TError = ErrorType<unknown>>(
|
||||
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getVersion>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getGetVersionQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getListToolsUrl = (params?: ListToolsParams,) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
|
||||
@@ -37,6 +37,20 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthStatus"
|
||||
|
||||
/version:
|
||||
get:
|
||||
operationId: getVersion
|
||||
tags: [health]
|
||||
summary: Build version information
|
||||
description: Returns the running build version, commit SHA and build date
|
||||
responses:
|
||||
"200":
|
||||
description: Version information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VersionInfo"
|
||||
|
||||
/tools:
|
||||
get:
|
||||
operationId: listTools
|
||||
@@ -660,6 +674,19 @@ components:
|
||||
required:
|
||||
- status
|
||||
|
||||
VersionInfo:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
commitSha:
|
||||
type: ["string", "null"]
|
||||
buildDate:
|
||||
type: ["string", "null"]
|
||||
trashRetentionDays:
|
||||
type: integer
|
||||
|
||||
AuthMode:
|
||||
type: object
|
||||
required: [mode]
|
||||
|
||||
@@ -17,6 +17,18 @@ export const HealthCheckResponse = zod.object({
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Returns the running build version, commit SHA and build date
|
||||
* @summary Build version information
|
||||
*/
|
||||
export const GetVersionResponse = zod.object({
|
||||
"version": zod.string(),
|
||||
"commitSha": zod.string().nullish(),
|
||||
"buildDate": zod.string().nullish(),
|
||||
"trashRetentionDays": zod.number().optional()
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @summary List all tools
|
||||
*/
|
||||
|
||||
@@ -46,3 +46,4 @@ export * from './userRoleUpdate';
|
||||
export * from './userRoleUpdateRole';
|
||||
export * from './userRoleUpdateTier';
|
||||
export * from './userTier';
|
||||
export * from './versionInfo';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.9.1 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* ToolRate API — Tool listing and rating platform
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
/** @nullable */
|
||||
commitSha?: string | null;
|
||||
/** @nullable */
|
||||
buildDate?: string | null;
|
||||
trashRetentionDays?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user