--- phase: quick-260914-ku1 verified: 2026-09-14T13:59:19Z status: passed score: 8/8 must-haves verified covered_files: - ".gitea/scripts/publish-images.sh" - ".gitea/workflows/ci.yml" - ".planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-PLAN.md" - ".planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-SUMMARY.md" - "apps/api/Dockerfile" - "apps/api/src/health/app-version.ts" - "apps/api/src/health/health.controller.spec.ts" - "apps/api/src/health/health.controller.ts" - "apps/api/src/main.ts" - "apps/web/Dockerfile" - "apps/web/src/components/layout/app-version-badge.test.tsx" - "apps/web/src/components/layout/app-version-badge.tsx" - "apps/web/src/components/layout/sidebar.test.tsx" - "apps/web/src/components/layout/sidebar.tsx" - "apps/web/src/lib/app-version.test.ts" - "apps/web/src/lib/app-version.ts" - "apps/web/src/messages/de.json" - "apps/web/src/messages/en.json" - "docker-compose.prod.yml" - "docs/anleitung-betrieb.md" - "docs/ci-cd-setup.md" - "packages/shared/src/index.ts" covered_digest: "v1:sha256:3cc012949ab9484c9c9821c7dea15392439adc9224bd2f9e3025991c0eba6062" behavior_unverified: 0 overrides_applied: 0 --- # Quick-Task 260914-ku1: Zwei Auslieferungskanaele (Beta/Live) — Verifikationsbericht **Ziel:** Zwei Auslieferungskanaele (main -> beta+latest; Tag v* -> live+vX.Y.Z; Zweig live ohne Tag nur geprueft), Versionsstempel per Build-Args in beide Container-Abbilder, `GET /health/version`, Versionsabzeichen in der Seitenleiste, `IMAGE_TAG` in docker-compose.prod.yml, Publish-Skript mit `--print-plan`, Betriebshandbuch Kapitel „Zwei Kanaele" inkl. Hotfix-Rezept und Live-Server-Einrichtung, ci-cd-setup.md aktualisiert, gepusht, echter CI-Lauf gruen. **Verifiziert:** 2026-09-14T13:59:19Z **Status:** passed **Re-Verifikation:** Nein — Erstverifikation Alle Pruefungen wurden selbst erneut ausgefuehrt (nicht aus dem SUMMARY uebernommen). Wo die eigene Messung von der SUMMARY-Behauptung abweicht, ist das unten vermerkt — es gab keine Abweichung. ## Pruefung 1 — Commits, Diff-Umfang, unangetastete Dateien | Befehl | Erwartet | Gemessen | |---|---|---| | `git log --oneline 1cd4212..HEAD` | 3 Commits | `ea6aa99`, `9731501`, `cdb571c` — 3 Commits | | `git diff --stat 6c19451 -- . ':!.planning'` (letzte Zeile) | `20 files changed` | `20 files changed, 851 insertions(+), 49 deletions(-)` | | `git diff --name-only 6c19451 -- '.env*' apps/api/prisma/schema.prisma apps/api/prisma/migrations package.json pnpm-lock.yaml biome.json apps/web/src/components/layout/sidebar-footer.tsx` | leer | leer (keine Ausgabe) | | `git status --porcelain` (vor jeder Aenderung durch die Verifikation) | nur die zwei erwarteten offenen Dateien | `M .planning/STATE.md`, `?? .../260914-ku1-SUMMARY.md` — beides die dem Orchestrator gehoerenden, unberuehrt gelassenen Dateien | | `git fetch -q && git status -sb \| head -1` | kein `[ahead` | `## main...origin/main` | Status: ✓ VERIFIED ## Pruefung 2 — Testsuiten und Typpruefung | Befehl | Erwartet | Gemessen | |---|---|---| | `cd apps/api && npx vitest run` | 65 Dateien / 1060 Tests | `Test Files 65 passed (65)` / `Tests 1060 passed (1060)` | | `cd apps/web && npx vitest run` | 40 Dateien / 243 Tests | `Test Files 40 passed (40)` / `Tests 243 passed (243)` | | `pnpm -C packages/shared exec tsc --noEmit` | Exit 0 | Exit 0 | | `pnpm -C apps/api exec tsc --noEmit` | Exit 0 | Exit 0 | | `pnpm -C apps/web exec tsc --noEmit` | Exit 0 | Exit 0 | Status: ✓ VERIFIED ## Pruefung 3 — Versionsstempel im Code (API und Web) Quelldateien gelesen (nicht nur gegrept): - `apps/api/src/health/health.controller.ts`: `getVersion()` delegiert an `getAppVersion()` aus `./app-version`, `@Public()` und `@Get('version')` gesetzt, kein Zugriff mehr auf `npm_package_version`. Kommentar begruendet T-KU1-03. - `apps/api/src/health/app-version.ts`: `getAppVersion()` liest `process.env.APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME` mit `||`-Vorgaben `dev`/`dev`/``/``, `name: 'tessera'`. `formatAppVersionLine()` baut `Tessera API () `, ohne Commit ohne Leerzeichen am Ende. - `apps/api/src/health/health.controller.spec.ts`: 6 Tests wie im Plan beschrieben (`check()`, Vorgaben, Durchreichen, Compose-Leerstring-Semantik, `formatAppVersionLine`, `@Public()`-Metadatenpruefung per `Reflect.getMetadata`). Alle 6 gruen (siehe Pruefung 2). - `apps/api/src/main.ts`: `console.log(formatAppVersionLine())` direkt nach der bestehenden Port-Zeile (Zeile 35, nach Zeile 34). - `apps/web/src/lib/app-version.ts`: `appVersion` liest `process.env.NEXT_PUBLIC_APP_VERSION/_CHANNEL/_COMMIT` je mit vollem Literalnamen (keine Destrukturierung, kein `process.env[name]`); `normalizeChannel` faellt bei unbekanntem Kanal auf `'dev'` zurueck; `loadApiVersion()` memoisiert ein Modul-Promise gegen `${API_URL}/health/version` mit `credentials: 'include'`, still bei Fehler (`.catch(() => null)`). - `apps/web/src/components/layout/app-version-badge.tsx`: `data-testid="app-version"`, Text `${appVersion.version} · ${t('channel.'+channel)}`, `title` aus `Commit ` und `API ()`, `undefined` wenn beides fehlt. - `apps/web/src/components/layout/sidebar.tsx`: Badge-Block liegt NACH dem Einklapp-Block (der `hidden md:block` traegt) und OHNE dieses Attribut — dadurch auch in der mobilen Schublade sichtbar; `!isCollapsed` blendet ihn eingeklappt aus (Zeilen ~201-206). - `apps/web/src/messages/de.json` / `en.json`: `sidebar.channel = { beta: "Beta", live: "Live", dev: "Entwicklung"/"Development" }` — in beiden Dateien vorhanden, per Node geprueft. - `sidebar.test.tsx`: Modul-Mock fuer `AppVersionBadge` und ein sechster Test `renders the version badge below the navigation`, der `screen.getByTestId('app-version-badge')` prueft. Status: ✓ VERIFIED ## Pruefung 4 — Dockerfile-Falsifizierung (unabhaengig wiederholt) Beide Dockerfiles gelesen: globales `ARG APP_VERSION=dev` (und die drei weiteren) vor dem ersten `FROM`; im Web-`builder` `ARG`-Wiederholung + `ENV NEXT_PUBLIC_APP_*` unmittelbar VOR `RUN pnpm --filter=@tessera/web build`; im `runner` beider Images `ARG`-Wiederholung + `ENV APP_VERSION=...` nach `ENV NODE_ENV=production`. Eigener Bau (nicht aus dem SUMMARY uebernommen): ``` docker build -f apps/api/Dockerfile --build-arg APP_VERSION=v7.7.7-verify --build-arg APP_CHANNEL=live -t tessera-api-verify:tmp . docker run --rm --entrypoint node tessera-api-verify:tmp -e 'console.log(process.env.APP_VERSION, process.env.APP_CHANNEL)' -> v7.7.7-verify live ``` Erwartung erfuellt. Danach `docker rmi tessera-api-verify:tmp` ausgefuehrt — kein Rueckstand (`docker images | grep -c tessera-api-verify` -> 0). Status: ✓ VERIFIED ## Pruefung 5 — CI-Workflow-Struktur und Publish-Skript `.gitea/workflows/ci.yml` per js-yaml geparst: ``` branches=["main","live"] tags=["v*"] jobs=quality,test,publish fetchDepth=0 script=true needs=test ``` `.gitea/scripts/publish-images.sh --print-plan` in drei Lagen (eigene Ausfuehrung): | GITHUB_REF | Ergebnis | |---|---| | `refs/heads/main` | Kanal `beta`, Push-Zeilen fuer `web:beta`, `web:latest`, `api:beta`, `api:latest` (main-Fall enthaelt BEIDE, `beta` und `latest`) | | `refs/heads/live` | „Kein Veroeffentlichungs-Anlass ... nichts zu tun." — keine `push`-Zeile | | `refs/tags/v1.0.0` | Kanal `live`, Push-Zeilen fuer `web:live`, `web:v1.0.0`, `api:live`, `api:v1.0.0` | Status: ✓ VERIFIED ## Pruefung 6 — CI-gebaute Abbilder und echter Gitea-Lauf `docker images | grep tessera-ctl` zeigt `localhost:3002/schalli/tessera-ctl/{api,web}:{beta,latest}` (zusaetzlich `git.vicolab.de/...` als Fetch-Alias und lokale `tessera-ctl-{api,web}:latest` aus fruehreren lokalen Bauten — nicht Teil dieser Pruefung). ``` docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e 'console.log(process.env.APP_VERSION, process.env.APP_CHANNEL, process.env.APP_COMMIT)' -> ea6aa99 beta ea6aa99 docker run --rm --entrypoint sh localhost:3002/schalli/tessera-ctl/web:beta -c 'grep -rl "ea6aa99" apps/web/.next/static | wc -l' -> 1 ``` `ea6aa99` ist der zuletzt gepushte Commit (siehe Pruefung 1) — Uebereinstimmung. Gitea-API (`GET /api/v1/repos/schalli/tessera-ctl/actions/runs?limit=5`, Token aus `git remote get-url --push origin`, nicht ausgegeben): ``` 297 ea6aa995b27de1dfa9b557c06df9fa3bbcdd8ea3 completed success push 296 6c19451be9a25feb227af075076a839a968c5366 completed success push ... ``` Lauf 297 entspricht `head_sha = ea6aa99...`, `status: completed`, `conclusion: success` — deckt sich mit der SUMMARY-Angabe. Status: ✓ VERIFIED ## Pruefung 7 — docker-compose.prod.yml / IMAGE_TAG ``` docker compose -f docker-compose.prod.yml config --images -> git.vicolab.de/schalli/tessera-ctl/web:beta, .../api:beta, postgres:16-alpine IMAGE_TAG=live docker compose -f docker-compose.prod.yml config --images -> .../api:live, postgres:16-alpine, .../web:live ``` Ohne Variable Vorgabe `beta`, mit `IMAGE_TAG=live` `live` fuer beide Images. Kein `:latest` mehr in der Datei (per grep unabhaengig bestaetigt: 0 Treffer). Status: ✓ VERIFIED ## Pruefung 8 — Betriebshandbuch und ci-cd-setup.md `docs/anleitung-betrieb.md`, Abschnitt `## 9. Zwei Kanäle: Live und Beta` (Zeile 355) gelesen (nicht nur gegreppt): erklaert den Kanalbegriff, die eine `.env`-Zeile je Server (`IMAGE_TAG=beta`/`IMAGE_TAG=live`), die Server-Compose-Anpassung, `pull` + `up -d --force-recreate`, das Freigabe-Rezept, den vollstaendigen Hotfix-Ablauf mit der fett gesetzten Regel „Keine Datenbankänderung als Hotfix" samt Alltagssprache-Begruendung (Migrations-Zeitstempel-Reihenfolge), die drei Erkennungswege (Oberflaeche/`curl`/Log) und die Einrichtung des neuen Live-Servers (eigene Secrets, eigene leere Datenbank, `APP_URL`, Reihenfolge Erstfreigabe vor erstem Pull). Echte Umlaute durchgaengig (`Zwei Kanäle`, `änderungen`, `möglicherweise`, `größeren` etc.) — Ton der Datei gehalten, keine ASCII-Umschrift in diesem Kapitel. `docs/ci-cd-setup.md`: Abschnitt „Gitea Secrets" beschreibt `REGISTRY_TOKEN` und den Login (kein „keine Secrets" mehr); Abschnitt „Pipeline-Ueberblick" beschreibt Trigger (main/live/Tags v*), Etiketten-Tabelle, Build-Args-Tabelle, laengere Web-Bauzeit und markiert D-13 als ueberholt. `grep -c "Kein Registry-Push\|build-deploy"` -> 0. | Grep | Erwartet | Gemessen | |---|---|---| | `^## 9\. Zwei Kanäle: Live und Beta` in anleitung-betrieb.md | 1 | 1 | | `IMAGE_TAG` in anleitung-betrieb.md | >= 6 | 11 | | `Keine Datenbankänderung als Hotfix` | 1 | 1 | | `Kein Registry-Push\|build-deploy` in ci-cd-setup.md | 0 | 0 | | `[äöüÄÖÜß]` in ci-cd-setup.md (ASCII-Konvention) | 0 | 0 | Status: ✓ VERIFIED ## Anti-Pattern-Scan Alle 13 durch die Fingerprint-Liste erfassten Code-/Config-Dateien auf `TBD`, `FIXME`, `XXX`, `TODO`, `HACK`, `PLACEHOLDER`, „not yet implemented" u. ae. geprueft — keine Treffer in einer der geaenderten Dateien. ## Requirements-Abdeckung `QUICK-260914-KU1` ist eine Quick-Task-Anforderung ohne eigenen Eintrag in `.planning/REQUIREMENTS.md` (projektueblich fuer `/gsd-quick`-Auftraege, kein Roadmap-Phasenbezug) — kein verwaistes Requirement, da REQUIREMENTS.md keinen Phasen-Bezug fuer diesen Quick-Task erwartet. ## Beobachtete Nebenpunkte (keine Gaps) - Zusaetzliche lokale Docker-Images (`git.vicolab.de/...:latest`, `tessera-ctl-api:latest`, `tessera-ctl-web:latest`) liegen auf dem Host aus fruehreren Bauten/Pulls — nicht Teil dieses Plans und nicht durch ihn verursacht. - `sidebar-footer.tsx` bleibt wie geplant unangetastet (toter Code, im SUMMARY als Nebenbefund vermerkt). - `.env.prod.example` bewusst ohne `IMAGE_TAG`-Zeile (Regel: keine `.env*`-Aenderungen) — im Handbuch als offener Punkt vermerkt. ## Angenommene Risiken - **T-KU1-03** (`GET /health/version` bleibt `@Public()`, gibt Version/Kanal/Commit/Bauzeit ohne Anmeldung preis): als "accept" im Threat-Register des Plans geflaggt, durch Spec-Test 6 gepinnt. Bei externem Verkauf von Tessera muesste das revidiert werden (im Plan bereits als Folgeaenderung genannt). Die Verifikation bestaetigt nur, dass die Entscheidung wie dokumentiert umgesetzt und getestet ist — keine eigene Bewertung des Risikos selbst. - **T-KU1-04** (Tag-Push `v*` als alleiniger Freigabe-Hebel, kein Tag-/Branch-Schutz in Gitea): als "accept" geflaggt, weil aktuell nur ein Konto Schreibrecht hat. Diese Verifikation hat KEINE Gitea-Repository-Einstellungen aendern koennen/muessen (ausserhalb der Erlaubnisliste) und bestaetigt nur, dass die Empfehlung im Handbuch/ci-cd-setup steht. - **`live`-Zweig und Tag `v1.0.0` sind bewusst nicht angelegt** — laut Plan Folgearbeit nach dem noch ausstehenden „Fehler-melden-Knopf"-Quick-Task. Das bedeutet: der Live-Kanal ist bislang nur durch die drei `--print-plan`-Simulationen und den lokalen Docker-Falsifizierungslauf bewiesen, NICHT durch einen echten CI-Lauf mit `refs/tags/v*` (der echte CI-Lauf in Pruefung 6 deckt nur den Beta-Pfad ab). Das ist im Rahmen des Plans ausdruecklich so vorgesehen (Output-Abschnitt: "Zweig live und Tag v1.0.0 werden NICHT in diesem Plan angelegt") und daher kein Gap, aber ein offener Punkt fuer die naechste Freigabe. - Zusaetzliche, vom Runner gebaute lokale Images auf dem Host (`git.vicolab.de/...:latest` etc.) wurden nicht aufgeraeumt — sie stammen nicht aus dieser Verifikation und wurden nicht entfernt, um den Host-Zustand nicht ueber das Mandat hinaus zu veraendern. --- _Verifiziert: 2026-09-14T13:59:19Z_ _Verifier: Claude (gsd-verifier)_