feat(admin): VERSION+sha в UI, bake CI и auto stage по sha-*
CI / test (push) Successful in 6m45s
CI / push-image (push) Failing after 4m38s
CI / ci-done (push) Has been cancelled

This commit is contained in:
2026-07-14 21:25:35 +03:00
parent b7d572ea8e
commit 2a32465050
9 changed files with 128 additions and 27 deletions
+4 -3
View File
@@ -1,3 +1,4 @@
VITE_API_BASE_URL=https://admin-api.dev.eventhub.local
VITE_WS_URL=wss://admin-ws.dev.eventhub.local
VITE_APP_TITLE=EventHub Admin
# Same-origin через Vite proxy (см. vite.config.ts) — иначе CORS с localhost → admin-api.
VITE_API_BASE_URL=
VITE_WS_URL=
VITE_APP_TITLE=EventHub Admin
+13
View File
@@ -30,6 +30,19 @@ jobs:
with:
node-version: '22'
- name: Bake build identity
id: build_meta
run: |
set -euo pipefail
APP_VERSION="$(tr -d '[:space:]' < VERSION 2>/dev/null || echo 0.0)"
GIT_SHA="${GITHUB_SHA:0:12}"
BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "VITE_APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV"
echo "VITE_GIT_SHA=${GIT_SHA}" >> "$GITHUB_ENV"
echo "VITE_BUILT_AT=${BUILT_AT}" >> "$GITHUB_ENV"
echo "version=${APP_VERSION}" >> "$GITHUB_OUTPUT"
echo "git_sha=${GIT_SHA}" >> "$GITHUB_OUTPUT"
- run: npm ci
- run: npm run lint
- run: npm run build
+38 -22
View File
@@ -1,40 +1,46 @@
name: Deploy stage (admin)
# Auto после успешного CI на master: тот же sha-*, плюс floating :stage.
# Product version (VERSION) не бампается — только sha в рантайме/registry.
on:
push:
tags:
- 'v*'
workflow_run:
workflows: [CI]
types: [completed]
branches: [master, main]
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag (например v0.1.1)'
required: true
default: 'v0.1.1'
sha_tag:
description: 'Образ sha-* (например sha-3f5dde498652)'
required: true
default: 'sha-3f5dde498652'
concurrency:
group: eventhub-frontadmin-deploy-stage
cancel-in-progress: false
env:
REGISTRY: git.sabilin.com/eventhub
jobs:
deploy-stage-admin:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
- name: Compute image tags
- name: Compute image tag
id: meta
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "sha_tag=${{ github.event.inputs.sha_tag }}" >> "$GITHUB_OUTPUT"
echo "release_tag=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "workflow_run" ]; then
SHA="${{ github.event.workflow_run.head_sha }}"
else
echo "sha_tag=sha-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
echo "release_tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
SHA="${GITHUB_SHA}"
fi
echo "tag=sha-${SHA:0:12}" >> "$GITHUB_OUTPUT"
- name: Wait for Docker daemon
run: bash scripts/ensure-docker-daemon.sh 240
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -46,14 +52,24 @@ jobs:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Promote admin-ui image (no rebuild)
- name: Verify admin-ui image from CI
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"
if ! docker buildx imagetools inspect "${REGISTRY}/eventhub-admin-ui:${TAG}" >/dev/null 2>&1; then
echo "Образ eventhub-admin-ui:${TAG} не найден — CI push не завершился."
exit 1
fi
echo "OK: eventhub-admin-ui:${TAG}"
- name: Promote sha → :stage (no rebuild)
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
bash scripts/clone-devops-run.sh scripts/promote-images.sh \
"${{ steps.meta.outputs.sha_tag }}" \
"${{ steps.meta.outputs.release_tag }}" \
"${{ steps.meta.outputs.tag }}" \
"${{ steps.meta.outputs.tag }}" \
stage \
eventhub-admin-ui
@@ -66,7 +82,7 @@ jobs:
script: |
export REGISTRY_USER='${{ secrets.REGISTRY_USER }}'
export REGISTRY_PASSWORD='${{ secrets.REGISTRY_PASSWORD }}'
bash /opt/eventhub-stage/devops/scripts/deploy-service.sh stage admin ${{ steps.meta.outputs.release_tag }}
bash /opt/eventhub-stage/devops/scripts/deploy-service.sh stage admin ${{ steps.meta.outputs.tag }}
- name: Prune old registry images
continue-on-error: true
+1
View File
@@ -0,0 +1 @@
0.1
+16
View File
@@ -0,0 +1,16 @@
import apiClient from './client';
export type AdminHealthInfo = {
status: string;
service?: string;
version?: string;
git_sha?: string;
built_at?: string;
};
export const healthApi = {
getAdminHealth: async (): Promise<AdminHealthInfo> => {
const { data } = await apiClient.get<AdminHealthInfo>('/v1/admin/health');
return data;
},
};
+39
View File
@@ -52,6 +52,12 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
import { CommandPalette } from '@/components/CommandPalette';
import { useCommandPaletteHotkey } from '@/hooks/useCommandPaletteHotkey';
import { HeaderNodeMetrics } from '@/components/HeaderNodeMetrics';
import { healthApi } from '@/api/healthApi';
function formatBuildLabel(version: string, sha: string): string {
const shortSha = sha && sha !== 'unknown' && sha !== 'dev' ? sha.slice(0, 12) : sha;
return `${version} (${shortSha})`;
}
const navIconByKey: Record<string, React.ComponentType<{ className?: string }>> = {
'/dashboard': LayoutDashboard,
@@ -102,10 +108,34 @@ const ControlCenterLayout: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const [paletteOpen, setPaletteOpen] = useState(false);
const [density, setDensity] = useState<TableDensity>(() => getTableDensity());
const [apiVersionLabel, setApiVersionLabel] = useState<string | null>(null);
const openPalette = useCallback(() => setPaletteOpen(true), []);
useCommandPaletteHotkey(openPalette);
const adminVersionLabel = formatBuildLabel(
import.meta.env.VITE_APP_VERSION || '0.0',
import.meta.env.VITE_GIT_SHA || 'dev'
);
useEffect(() => {
let cancelled = false;
healthApi
.getAdminHealth()
.then((info) => {
if (cancelled) return;
setApiVersionLabel(
formatBuildLabel(info.version || '0.0', info.git_sha || 'unknown')
);
})
.catch(() => {
if (!cancelled) setApiVersionLabel(null);
});
return () => {
cancelled = true;
};
}, []);
const selectedKey = getMenuSelectedKey(location.pathname);
const groups = useMemo(() => filterNavGroupsByRole(NAV_GROUPS, user?.role), [user?.role]);
@@ -317,6 +347,15 @@ const ControlCenterLayout: React.FC = () => {
{t('common.langEn')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="space-y-0.5 text-xs font-normal text-muted-foreground">
<div>{t('layout.adminVersion', { version: adminVersionLabel })}</div>
<div>
{apiVersionLabel
? t('layout.apiVersion', { version: apiVersionLabel })
: t('layout.apiVersionUnavailable')}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-destructive focus:text-destructive">
<LogOut className="mr-2 h-4 w-4" />
{t('layout.logout')}
+4 -1
View File
@@ -192,7 +192,10 @@
"nodeCpu": "CPU: {{value}}%",
"nodeMemory": "Memory: {{used}} / {{total}} MB ({{percent}}%)",
"nodeWas": "was {{value}}%",
"nodeAria": "{{node}}: CPU {{cpu}}%, memory {{memory}}%"
"nodeAria": "{{node}}: CPU {{cpu}}%, memory {{memory}}%",
"adminVersion": "Admin {{version}}",
"apiVersion": "API {{version}}",
"apiVersionUnavailable": "API — unavailable"
},
"explore": {
"viewAria": "List view",
+4 -1
View File
@@ -192,7 +192,10 @@
"nodeCpu": "CPU: {{value}}%",
"nodeMemory": "Память: {{used}} / {{total}} МБ ({{percent}}%)",
"nodeWas": "было {{value}}%",
"nodeAria": "{{node}}: CPU {{cpu}}%, память {{memory}}%"
"nodeAria": "{{node}}: CPU {{cpu}}%, память {{memory}}%",
"adminVersion": "Admin {{version}}",
"apiVersion": "API {{version}}",
"apiVersionUnavailable": "API — нет данных"
},
"explore": {
"viewAria": "Вид списка",
+9
View File
@@ -6,8 +6,17 @@ import tailwindcss from '@tailwindcss/vite';
const apiBaseUrl = process.env.VITE_API_BASE_URL ?? 'https://admin-api.dev.eventhub.local';
const wsUrl = process.env.VITE_WS_URL ?? 'wss://admin-ws.dev.eventhub.local';
const appVersion = process.env.VITE_APP_VERSION ?? '0.0';
const gitSha = process.env.VITE_GIT_SHA ?? 'dev';
const builtAt = process.env.VITE_BUILT_AT ?? '';
export default defineConfig({
plugins: [react(), tailwindcss()],
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
'import.meta.env.VITE_GIT_SHA': JSON.stringify(gitSha),
'import.meta.env.VITE_BUILT_AT': JSON.stringify(builtAt),
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),