feat(quick-260914-ku1): Versionsstempel — GET /health/version aus APP_*, VersionResponse, app-version.ts und Abzeichen v<Version> · <Kanal> in der Seitenleiste
- packages/shared: VersionResponse { name, version, channel, commit, buildTime }
- apps/api: health/app-version.ts (getAppVersion mit ||-Vorgaben dev/dev, formatAppVersionLine),
HealthController.getVersion delegiert (bleibt @Public, T-KU1-03), main.ts protokolliert
"Tessera API <version> (<channel>) <commit>" beim Start; neuer Spec mit 6 Tests
- apps/web: lib/app-version.ts (NEXT_PUBLIC_APP_* mit vollem Literalnamen, loadApiVersion
memoisiert und still bei Fehler), AppVersionBadge unten in der Seitenleiste (auch mobil,
nicht eingeklappt), sidebar.channel.{beta,live,dev} in de/en; 5 + 4 + 1 neue Tests
- Suiten: API 65 Dateien / 1060 Tests, Web 40 / 243, tsc in shared/api/web Exit 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import type { VersionResponse } from '@tessera/shared';
|
||||
|
||||
/**
|
||||
* Versionsstempel der API (quick-260914-ku1).
|
||||
*
|
||||
* Woher die Werte kommen: das CI-Skript `.gitea/scripts/publish-images.sh`
|
||||
* berechnet `APP_VERSION` (`git describe --tags --always`), `APP_CHANNEL`
|
||||
* (`beta` fuer main, `live` fuer Tags v*), `APP_COMMIT` und `APP_BUILD_TIME`
|
||||
* und uebergibt sie als `--build-arg`; die Runner-Stufe beider Dockerfiles
|
||||
* setzt sie als `ENV`, sodass sie hier zur Laufzeit lesbar sind. Lokal ohne
|
||||
* Build-Args greifen die Vorgaben `dev`/`dev`/``/``.
|
||||
*
|
||||
* Warum `||` statt `??`: Docker Compose reicht unbelegte Variablen als
|
||||
* Leerstring weiter — ein leerer Wert muss wie ein fehlender zaehlen.
|
||||
*
|
||||
* `main.ts` protokolliert `formatAppVersionLine()` beim Start direkt nach der
|
||||
* Port-Zeile, `HealthController.getVersion()` liefert `getAppVersion()`.
|
||||
*/
|
||||
export function getAppVersion(): VersionResponse {
|
||||
return {
|
||||
name: 'tessera',
|
||||
version: process.env.APP_VERSION || 'dev',
|
||||
channel: process.env.APP_CHANNEL || 'dev',
|
||||
commit: process.env.APP_COMMIT || '',
|
||||
buildTime: process.env.APP_BUILD_TIME || '',
|
||||
};
|
||||
}
|
||||
|
||||
export function formatAppVersionLine(v: VersionResponse = getAppVersion()): string {
|
||||
const base = `Tessera API ${v.version} (${v.channel})`;
|
||||
return v.commit ? `${base} ${v.commit}` : base;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'reflect-metadata';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { IS_PUBLIC_KEY } from '../auth/decorators/public.decorator';
|
||||
import { formatAppVersionLine } from './app-version';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
/**
|
||||
* HealthController.spec — legt die Testlage fuer den Health-Bereich aus dem
|
||||
* Nichts an (quick-260914-ku1, Aufgabe 1: vorher gab es keinen Spec).
|
||||
*
|
||||
* Gegenstand ist der Versionsstempel: `GET /health/version` liest
|
||||
* `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME` aus der
|
||||
* Laufzeit-Umgebung (gesetzt als `ENV` in der Runner-Stufe beider
|
||||
* Dockerfiles, befuellt vom CI-Skript `.gitea/scripts/publish-images.sh`).
|
||||
* Leere Zeichenketten zaehlen wie ungesetzt, weil Docker Compose unbelegte
|
||||
* Variablen als Leerstring weiterreicht (siehe `migrate-and-start.sh`).
|
||||
*
|
||||
* Der Controller hat keine Abhaengigkeiten und wird direkt instanziiert.
|
||||
*/
|
||||
|
||||
const ALL_VARS = ['APP_VERSION', 'APP_CHANNEL', 'APP_COMMIT', 'APP_BUILD_TIME'] as const;
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
function stubAll(value: string) {
|
||||
for (const name of ALL_VARS) {
|
||||
vi.stubEnv(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
function stubSample() {
|
||||
vi.stubEnv('APP_VERSION', 'v1.2.3');
|
||||
vi.stubEnv('APP_CHANNEL', 'live');
|
||||
vi.stubEnv('APP_COMMIT', 'abc1234');
|
||||
vi.stubEnv('APP_BUILD_TIME', '2026-09-14T12:00:00Z');
|
||||
}
|
||||
|
||||
describe('HealthController — Versionsstempel (quick-260914-ku1)', () => {
|
||||
it('Test 1 (check): liefert status ok und einen Zeitstempel, den Date.parse versteht', () => {
|
||||
const controller = new HealthController();
|
||||
const result = controller.check();
|
||||
expect(result.status).toBe('ok');
|
||||
expect(Number.isNaN(Date.parse(result.timestamp))).toBe(false);
|
||||
});
|
||||
|
||||
it('Test 2 (Vorgaben): ohne die vier Variablen liefert getVersion() dev/dev und leere Kennungen', () => {
|
||||
for (const name of ALL_VARS) {
|
||||
vi.stubEnv(name, undefined);
|
||||
}
|
||||
const controller = new HealthController();
|
||||
expect(controller.getVersion()).toEqual({
|
||||
name: 'tessera',
|
||||
version: 'dev',
|
||||
channel: 'dev',
|
||||
commit: '',
|
||||
buildTime: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('Test 3 (durchgereicht): die vier Variablen kommen woertlich an, name bleibt tessera', () => {
|
||||
stubSample();
|
||||
const controller = new HealthController();
|
||||
expect(controller.getVersion()).toEqual({
|
||||
name: 'tessera',
|
||||
version: 'v1.2.3',
|
||||
channel: 'live',
|
||||
commit: 'abc1234',
|
||||
buildTime: '2026-09-14T12:00:00Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('Test 4 (Compose-Semantik): leere Zeichenketten zaehlen wie ungesetzt', () => {
|
||||
stubAll('');
|
||||
const controller = new HealthController();
|
||||
expect(controller.getVersion()).toEqual({
|
||||
name: 'tessera',
|
||||
version: 'dev',
|
||||
channel: 'dev',
|
||||
commit: '',
|
||||
buildTime: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('Test 5 (formatAppVersionLine): mit Commit angehaengt, ohne Commit kein Leerzeichen am Ende', () => {
|
||||
stubSample();
|
||||
expect(formatAppVersionLine()).toBe('Tessera API v1.2.3 (live) abc1234');
|
||||
|
||||
stubAll('');
|
||||
expect(formatAppVersionLine()).toBe('Tessera API dev (dev)');
|
||||
});
|
||||
|
||||
it('Test 6 (bewusst oeffentlich, T-KU1-03): getVersion und check tragen @Public()', () => {
|
||||
expect(Reflect.getMetadata(IS_PUBLIC_KEY, HealthController.prototype.getVersion)).toBe(true);
|
||||
expect(Reflect.getMetadata(IS_PUBLIC_KEY, HealthController.prototype.check)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import type { HealthResponse } from '@tessera/shared';
|
||||
import type { HealthResponse, VersionResponse } from '@tessera/shared';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
import { getAppVersion } from './app-version';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@@ -13,12 +14,12 @@ export class HealthController {
|
||||
};
|
||||
}
|
||||
|
||||
// Bewusst oeffentlich (T-KU1-03): Betreiber-Kontrolle per `curl` auf dem
|
||||
// Server ohne Anmeldung. Es werden keine Versionen von Systemkomponenten
|
||||
// preisgegeben; das Repository ist privat. Gepinnt durch Spec-Test 6.
|
||||
@Public()
|
||||
@Get('version')
|
||||
getVersion() {
|
||||
return {
|
||||
version: process.env.npm_package_version ?? '0.0.1',
|
||||
name: 'tessera',
|
||||
};
|
||||
getVersion(): VersionResponse {
|
||||
return getAppVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { AppModule } from './app.module';
|
||||
import { formatAppVersionLine } from './health/app-version';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
@@ -31,6 +32,7 @@ async function bootstrap() {
|
||||
|
||||
await app.listen(3001);
|
||||
console.log('Tessera API running on port 3001');
|
||||
console.log(formatAppVersionLine());
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
Reference in New Issue
Block a user