feat(client): show git-describe version next to the brand logo

Display the app version (output of `git describe --tags`) in the header
beside the gitmost logo: a clean tag renders as `vX.Y.Z`, otherwise the
tag plus commits-since and short hash (e.g. v0.90.1-56-g25975acd).

- vite.config.ts: resolve APP_VERSION from env (Docker/CI) -> git describe
  (local) -> package.json version fallback
- app-header.tsx: render APP_VERSION after the brand block (ml="md"),
  nudge the Home nav group (ml={50} -> "xl")
- Dockerfile: accept APP_VERSION build-arg in the builder stage (.git is
  excluded from the build context)
- CI: pass APP_VERSION build-arg — release.yml uses the tag, develop.yml
  computes git describe with fetch-depth: 0
- nx.json: add APP_VERSION to the build target inputs so the cache
  invalidates when the version changes
This commit is contained in:
vvzvlad
2026-06-18 04:51:21 +03:00
parent ea56985efd
commit a0a8d3c97f
6 changed files with 58 additions and 3 deletions

View File

@@ -23,6 +23,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -34,11 +36,17 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve version
id: version
run: echo "value=$(git describe --tags --always)" >> "$GITHUB_OUTPUT"
- name: Build and push develop image - name: Build and push develop image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
build-args: |
APP_VERSION=${{ steps.version.outputs.value }}
push: true push: true
tags: ${{ env.IMAGE }}:develop tags: ${{ env.IMAGE }}:develop
cache-from: type=gha,scope=develop-amd64 cache-from: type=gha,scope=develop-amd64

View File

@@ -50,6 +50,8 @@ jobs:
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
build-args: |
APP_VERSION=${{ env.VERSION }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.suffix }} cache-from: type=gha,scope=${{ matrix.suffix }}
cache-to: type=gha,scope=${{ matrix.suffix }},mode=max,ignore-error=true cache-to: type=gha,scope=${{ matrix.suffix }},mode=max,ignore-error=true
@@ -76,6 +78,8 @@ jobs:
with: with:
context: . context: .
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
build-args: |
APP_VERSION=${{ env.VERSION }}
push: false push: false
tags: | tags: |
${{ env.IMAGE }}:latest ${{ env.IMAGE }}:latest

View File

@@ -10,6 +10,9 @@ WORKDIR /app
COPY . . COPY . .
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
# Version string shown in the UI (computed outside Docker because .git is not in the build context).
ARG APP_VERSION=""
ENV APP_VERSION=$APP_VERSION
RUN pnpm build RUN pnpm build
FROM base AS installer FROM base AS installer

View File

@@ -1,6 +1,7 @@
import { import {
Box, Box,
Group, Group,
Text,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import classes from "./app-header.module.css"; import classes from "./app-header.module.css";
@@ -76,7 +77,20 @@ export function AppHeader() {
</Box> </Box>
</Link> </Link>
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm"> <Tooltip label={t("Version")}>
<Text
size="xs"
c="dimmed"
lh={1}
ml="md"
visibleFrom="sm"
style={{ userSelect: "text", whiteSpace: "nowrap" }}
>
{APP_VERSION}
</Text>
</Tooltip>
<Group ml="xl" gap={5} className={classes.links} visibleFrom="sm">
{items} {items}
</Group> </Group>
</Group> </Group>

View File

@@ -1,9 +1,28 @@
import { defineConfig, loadEnv } from "vite"; import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import * as path from "path"; import * as path from "path";
import { execSync } from "node:child_process";
const envPath = path.resolve(process.cwd(), "..", ".."); const envPath = path.resolve(process.cwd(), "..", "..");
// Resolve the version string shown in the UI.
// Priority: explicit APP_VERSION env (injected by Docker/CI, where .git is absent),
// then `git describe` for local builds, then the package.json version as a fallback.
function resolveAppVersion(cwd: string): string {
const fromEnv = process.env.APP_VERSION?.trim();
if (fromEnv) return fromEnv;
try {
return execSync("git describe --tags --always", {
cwd,
stdio: ["ignore", "pipe", "ignore"],
})
.toString()
.trim();
} catch {
return `v${process.env.npm_package_version ?? "0.0.0"}`;
}
}
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const { const {
APP_URL, APP_URL,
@@ -32,7 +51,7 @@ export default defineConfig(({ mode }) => {
POSTHOG_HOST, POSTHOG_HOST,
POSTHOG_KEY, POSTHOG_KEY,
}, },
APP_VERSION: JSON.stringify(process.env.npm_package_version), APP_VERSION: JSON.stringify(resolveAppVersion(envPath)),
}, },
plugins: [react()], plugins: [react()],
build: { build: {

View File

@@ -4,7 +4,14 @@
"dependsOn": [ "dependsOn": [
"^build" "^build"
], ],
"cache": true "cache": true,
"inputs": [
"default",
"^default",
{
"env": "APP_VERSION"
}
]
}, },
"start:dev": { "start:dev": {
"dependsOn": [ "dependsOn": [