feat(infra): Unified Build Script fuer alle Komponenten #206

Closed
opened 2026-07-03 19:02:03 +02:00 by niboer · 1 comment
Owner

Ziel

Den Build-Prozess fuer Backend, Frontend, Keycloak und E2E-Tests vereinheitlichen. Jede Komponente baut in ihrem eigenen Build-Context — externe Abhaengigkeiten (shared/) werden vom Build-Script vorab ins Component-Verzeichnis kopiert.

Scope: build.mjs ist fuer die Entwicklung (lokale Images). Produktions-Images kommen via CI/CD in eine Registry und werden von dort gepullt.

Hintergrund

Drei Komponenten benoetigen shared/design-tokens/tokens.css ausserhalb ihres Verzeichnisses:

  • Frontend: frontend/src/app.css importiert ../../shared/design-tokens/tokens.css
  • Keycloak: tokens.css wird aktuell via Volume-Mount eingebunden
  • Backend: self-contained (keine externen Deps)

Statt den Build-Context aufzuweiten, kopiert scripts/prepare-tokens.mjs vor dem Docker-Build die benoetigten Dateien in jedes Component-Verzeichnis. Die Kopien liegen in .gitignore.

Image-Tags

Jeder Service bekommt einen eindeutigen lokalen Image-Namen:

Service Image-Tag
postgres postgres:latest (Remote, unveraendert)
mailhog mailhog/mailhog:v1.0.1 (Remote, unveraendert)
backend-api familienfeierplaner-backend (lokal)
frontend familienfeierplaner-frontend (lokal, NEU)
keycloak familienfeierplaner-keycloak (lokal, NEU)
e2e-runner familienfeierplaner-e2e (lokal, NEU)

Dadurch entfernt docker compose down --rmi local ausschliesslich die 4 selbst gebauten Images — postgres:latest und mailhog/mailhog bleiben erhalten.

Podman/Docker-Erkennung

build.mjs und docker-test.mjs nutzen eine gemeinsame Helper-Funktion aus scripts/container.mjs:

// scripts/container.mjs
import { execSync } from 'node:child_process';

export function detectContainerCmd() {
  const envCmd = process.env.CONTAINER_CMD;
  if (envCmd) return envCmd;
  for (const cmd of ['docker', 'podman']) {
    try { execSync(`${cmd} --version`, { stdio: 'pipe' }); return cmd; }
    catch { /* continue */ }
  }
  return 'docker';
}

Alle Scripts (build.mjs, docker-test.mjs) importieren von dort — kein Code-Duplikat.

Umsetzung

1. scripts/container.mjs (neu)

Gemeinsame Helper: detectContainerCmd(), CMD, COMPOSE.

2. scripts/prepare-tokens.mjs (neu)

Kopiert shared/design-tokens/tokens.css an drei Zielorte:

Ziel Erforderlich fuer
frontend/shared/design-tokens/tokens.css Frontend-Docker-Build (Import via ../shared/...)
keycloak/themes/.../login/resources/css/tokens.css Keycloak-Login-Theme
keycloak/themes/.../account/resources/css/tokens.css Keycloak-Account-Theme
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { resolve, dirname } from 'path';

const ROOT = resolve(import.meta.dirname, '..');
const SRC = resolve(ROOT, 'shared/design-tokens/tokens.css');
const TARGETS = [
  'frontend/shared/design-tokens/tokens.css',
  'keycloak/themes/familienfeierplaner/login/resources/css/tokens.css',
  'keycloak/themes/familienfeierplaner/account/resources/css/tokens.css',
];

if (existsSync(SRC)) {
  for (const rel of TARGETS) {
    const dest = resolve(ROOT, rel);
    mkdirSync(dirname(dest), { recursive: true });
    copyFileSync(SRC, dest);
  }
}

Wird sowohl von build.mjs als auch vom predev-Hook aufgerufen.

3. frontend/src/app.css (geaendert)

Import-Pfad anpassen:

/* ALT: ../../shared/design-tokens/tokens.css */
/* NEU: ../shared/design-tokens/tokens.css */
@import '../shared/design-tokens/tokens.css';

4. keycloak/Dockerfile (neu)

FROM keycloak/keycloak:latest
COPY themes/ /opt/keycloak/themes/

Build-Context: ./keycloak

5. e2e/Dockerfile (neu)

FROM mcr.microsoft.com/playwright:v1.52.0-jammy
WORKDIR /app
COPY package.json .
RUN npm install
COPY playwright.config.ts .
COPY specs/ ./specs/
COPY fixtures/ ./fixtures/
COPY helpers/ ./helpers/

Build-Context: ./e2e

6. .gitignore (geaendert)

frontend/shared/
keycloak/**/tokens.css

7. frontend/package.json (geaendert)

"scripts": {
  "predev":   "node ../scripts/prepare-tokens.mjs",
  "prebuild": "node ../scripts/prepare-tokens.mjs",
  "dev":   "vite dev",
  "build": "vite build"
}

8. docker-compose.yml (Aenderungen)

  • backend-api: unveraendert (image: familienfeierplaner-backend)
  • frontend: image: familienfeierplaner-frontend hinzugefuegt
  • keycloak: von image: keycloak/keycloak:latest auf build:, image: familienfeierplaner-keycloak, Volume-Mounts fuer tokens.css entfernt
  • e2e-runner: von image: mcr.microsoft.com/playwright:... auf build:, image: familienfeierplaner-e2e

9. scripts/build.mjs (neu)

Unified Build Orchestrator:

node scripts/build.mjs backend       # prepare-tokens → docker compose build backend-api
node scripts/build.mjs frontend      # prepare-tokens → docker compose build frontend
node scripts/build.mjs keycloak      # prepare-tokens → docker compose build keycloak
node scripts/build.mjs e2e           # docker compose build e2e-runner
node scripts/build.mjs all           # Alle in Reihenfolge

Optionen: --no-cache, --tag, --push (fuer CI/CD)

Podman-Unterstützung: via import { CMD, COMPOSE } from './container.mjs'

10. package.json (Root) — Scripts

"scripts": {
  "build":          "node scripts/build.mjs all",
  "build:backend":  "node scripts/build.mjs backend",
  "build:frontend": "node scripts/build.mjs frontend",
  "build:keycloak": "node scripts/build.mjs keycloak",
  "build:e2e":      "node scripts/build.mjs e2e",
  "dev":            "npm run build:backend && npm run build:keycloak && docker compose down -v --rmi local --remove-orphans && docker compose up postgres mailhog keycloak backend-api",
  "dev:reset":      "docker compose down -v && npm run dev"
}

npm run dev macht:

  1. prepare-tokens.mjs → Backend + Keycloak lokal bauen
  2. docker compose down -v --rmi local --remove-orphans — Container, Volumes, lokale Images entfernen
  3. docker compose up postgres mailhog keycloak backend-api — Stack sauber starten

11. scripts/docker-test.mjs (angepasst)

  • detectContainerCmd() aus docker-test.mjs entfernen, aus container.mjs importieren
  • Build-Schritt: build.mjs all statt docker compose up --build
  • Nach Build: docker compose --profile integration-test up -d (nur Start, ohne Build)

Workflow-Uebersicht

Aufgabe Befehl
Backend lokal bauen npm run build:backend
Backend-Stack starten (Build + Clean + Up) npm run dev
Backend-Stack + Frontend (Integrationstest) npm run build && docker compose --profile integration-test up -d
Kompletten Stack zurücksetzen npm run dev:reset
Container stoppen docker compose down
CI/CD-Build + Push node scripts/build.mjs all --push

Build-Reihenfolge

1. Backend    (self-contained, unveraendert)
2. Frontend   (prepare-tokens → docker compose build frontend)
3. Keycloak   (prepare-tokens → docker compose build keycloak)
4. e2e        (self-contained, docker compose build e2e-runner)

Voraussetzung

#204 (Frontend-Docker-Build) ist closed.

Dateien (neu/geaendert)

.gitignore                         # GEAENDERT
scripts/container.mjs              # NEU (Podman/Docker-Detection)
scripts/build.mjs                  # NEU
scripts/prepare-tokens.mjs         # NEU
keycloak/Dockerfile                # NEU
e2e/Dockerfile                     # NEU
frontend/src/app.css               # GEAENDERT
frontend/package.json              # GEAENDERT
docker-compose.yml                 # GEAENDERT
package.json                       # GEAENDERT
scripts/docker-test.mjs            # GEAENDERT
## Ziel Den Build-Prozess fuer Backend, Frontend, Keycloak und E2E-Tests vereinheitlichen. Jede Komponente baut in ihrem eigenen Build-Context — externe Abhaengigkeiten (`shared/`) werden vom Build-Script vorab ins Component-Verzeichnis kopiert. **Scope:** `build.mjs` ist fuer die **Entwicklung** (lokale Images). Produktions-Images kommen via CI/CD in eine Registry und werden von dort gepullt. ## Hintergrund Drei Komponenten benoetigen `shared/design-tokens/tokens.css` ausserhalb ihres Verzeichnisses: - Frontend: `frontend/src/app.css` importiert `../../shared/design-tokens/tokens.css` - Keycloak: `tokens.css` wird aktuell via Volume-Mount eingebunden - Backend: self-contained (keine externen Deps) Statt den Build-Context aufzuweiten, kopiert `scripts/prepare-tokens.mjs` vor dem Docker-Build die benoetigten Dateien in jedes Component-Verzeichnis. Die Kopien liegen in `.gitignore`. ## Image-Tags Jeder Service bekommt einen eindeutigen lokalen Image-Namen: | Service | Image-Tag | |---------|-----------| | postgres | `postgres:latest` (Remote, unveraendert) | | mailhog | `mailhog/mailhog:v1.0.1` (Remote, unveraendert) | | backend-api | `familienfeierplaner-backend` (lokal) | | frontend | `familienfeierplaner-frontend` (lokal, NEU) | | keycloak | `familienfeierplaner-keycloak` (lokal, NEU) | | e2e-runner | `familienfeierplaner-e2e` (lokal, NEU) | Dadurch entfernt `docker compose down --rmi local` ausschliesslich die 4 selbst gebauten Images — `postgres:latest` und `mailhog/mailhog` bleiben erhalten. ## Podman/Docker-Erkennung `build.mjs` und `docker-test.mjs` nutzen eine gemeinsame Helper-Funktion aus `scripts/container.mjs`: ```javascript // scripts/container.mjs import { execSync } from 'node:child_process'; export function detectContainerCmd() { const envCmd = process.env.CONTAINER_CMD; if (envCmd) return envCmd; for (const cmd of ['docker', 'podman']) { try { execSync(`${cmd} --version`, { stdio: 'pipe' }); return cmd; } catch { /* continue */ } } return 'docker'; } ``` **Alle Scripts** (`build.mjs`, `docker-test.mjs`) importieren von dort — kein Code-Duplikat. ## Umsetzung ### 1. `scripts/container.mjs` (neu) Gemeinsame Helper: `detectContainerCmd()`, `CMD`, `COMPOSE`. ### 2. `scripts/prepare-tokens.mjs` (neu) Kopiert `shared/design-tokens/tokens.css` an drei Zielorte: | Ziel | Erforderlich fuer | |------|-------------------| | `frontend/shared/design-tokens/tokens.css` | Frontend-Docker-Build (Import via `../shared/...`) | | `keycloak/themes/.../login/resources/css/tokens.css` | Keycloak-Login-Theme | | `keycloak/themes/.../account/resources/css/tokens.css` | Keycloak-Account-Theme | ```javascript import { copyFileSync, existsSync, mkdirSync } from 'fs'; import { resolve, dirname } from 'path'; const ROOT = resolve(import.meta.dirname, '..'); const SRC = resolve(ROOT, 'shared/design-tokens/tokens.css'); const TARGETS = [ 'frontend/shared/design-tokens/tokens.css', 'keycloak/themes/familienfeierplaner/login/resources/css/tokens.css', 'keycloak/themes/familienfeierplaner/account/resources/css/tokens.css', ]; if (existsSync(SRC)) { for (const rel of TARGETS) { const dest = resolve(ROOT, rel); mkdirSync(dirname(dest), { recursive: true }); copyFileSync(SRC, dest); } } ``` Wird sowohl von `build.mjs` als auch vom `predev`-Hook aufgerufen. ### 3. `frontend/src/app.css` (geaendert) Import-Pfad anpassen: ```css /* ALT: ../../shared/design-tokens/tokens.css */ /* NEU: ../shared/design-tokens/tokens.css */ @import '../shared/design-tokens/tokens.css'; ``` ### 4. `keycloak/Dockerfile` (neu) ```dockerfile FROM keycloak/keycloak:latest COPY themes/ /opt/keycloak/themes/ ``` **Build-Context:** `./keycloak` ### 5. `e2e/Dockerfile` (neu) ```dockerfile FROM mcr.microsoft.com/playwright:v1.52.0-jammy WORKDIR /app COPY package.json . RUN npm install COPY playwright.config.ts . COPY specs/ ./specs/ COPY fixtures/ ./fixtures/ COPY helpers/ ./helpers/ ``` **Build-Context:** `./e2e` ### 6. `.gitignore` (geaendert) ``` frontend/shared/ keycloak/**/tokens.css ``` ### 7. `frontend/package.json` (geaendert) ```json "scripts": { "predev": "node ../scripts/prepare-tokens.mjs", "prebuild": "node ../scripts/prepare-tokens.mjs", "dev": "vite dev", "build": "vite build" } ``` ### 8. `docker-compose.yml` (Aenderungen) - **backend-api:** unveraendert (`image: familienfeierplaner-backend`) - **frontend:** `image: familienfeierplaner-frontend` hinzugefuegt - **keycloak:** von `image: keycloak/keycloak:latest` auf `build:`, `image: familienfeierplaner-keycloak`, Volume-Mounts fuer tokens.css entfernt - **e2e-runner:** von `image: mcr.microsoft.com/playwright:...` auf `build:`, `image: familienfeierplaner-e2e` ### 9. `scripts/build.mjs` (neu) Unified Build Orchestrator: ```bash node scripts/build.mjs backend # prepare-tokens → docker compose build backend-api node scripts/build.mjs frontend # prepare-tokens → docker compose build frontend node scripts/build.mjs keycloak # prepare-tokens → docker compose build keycloak node scripts/build.mjs e2e # docker compose build e2e-runner node scripts/build.mjs all # Alle in Reihenfolge ``` Optionen: `--no-cache`, `--tag`, `--push` (fuer CI/CD) **Podman-Unterstützung:** via `import { CMD, COMPOSE } from './container.mjs'` ### 10. `package.json` (Root) — Scripts ```json "scripts": { "build": "node scripts/build.mjs all", "build:backend": "node scripts/build.mjs backend", "build:frontend": "node scripts/build.mjs frontend", "build:keycloak": "node scripts/build.mjs keycloak", "build:e2e": "node scripts/build.mjs e2e", "dev": "npm run build:backend && npm run build:keycloak && docker compose down -v --rmi local --remove-orphans && docker compose up postgres mailhog keycloak backend-api", "dev:reset": "docker compose down -v && npm run dev" } ``` **`npm run dev`** macht: 1. `prepare-tokens.mjs` → Backend + Keycloak lokal bauen 2. `docker compose down -v --rmi local --remove-orphans` — Container, Volumes, lokale Images entfernen 3. `docker compose up postgres mailhog keycloak backend-api` — Stack sauber starten ### 11. `scripts/docker-test.mjs` (angepasst) - `detectContainerCmd()` aus `docker-test.mjs` entfernen, aus `container.mjs` importieren - Build-Schritt: `build.mjs all` statt `docker compose up --build` - Nach Build: `docker compose --profile integration-test up -d` (nur Start, ohne Build) ## Workflow-Uebersicht | Aufgabe | Befehl | |---------|--------| | Backend lokal bauen | `npm run build:backend` | | Backend-Stack starten (Build + Clean + Up) | `npm run dev` | | Backend-Stack + Frontend (Integrationstest) | `npm run build && docker compose --profile integration-test up -d` | | Kompletten Stack zurücksetzen | `npm run dev:reset` | | Container stoppen | `docker compose down` | | CI/CD-Build + Push | `node scripts/build.mjs all --push` | ## Build-Reihenfolge ``` 1. Backend (self-contained, unveraendert) 2. Frontend (prepare-tokens → docker compose build frontend) 3. Keycloak (prepare-tokens → docker compose build keycloak) 4. e2e (self-contained, docker compose build e2e-runner) ``` ## Voraussetzung #204 (Frontend-Docker-Build) ist closed. ## Dateien (neu/geaendert) ``` .gitignore # GEAENDERT scripts/container.mjs # NEU (Podman/Docker-Detection) scripts/build.mjs # NEU scripts/prepare-tokens.mjs # NEU keycloak/Dockerfile # NEU e2e/Dockerfile # NEU frontend/src/app.css # GEAENDERT frontend/package.json # GEAENDERT docker-compose.yml # GEAENDERT package.json # GEAENDERT scripts/docker-test.mjs # GEAENDERT ```
niboer added this to the MS-INTEGRATION milestone 2026-07-03 19:02:03 +02:00
niboer self-assigned this 2026-07-03 19:02:03 +02:00
Author
Owner

Ergaenzung: Builder-Intermediate-Image wird nicht aufgeraeumt

Beobachtung: Nach jedem docker compose down -v --rmi local bleibt ein unbenannter Intermediate-Image der builder-Stage aus dem Multi-Stage-Dockerfile (backend/Dockerfile) zurueck.

Ursache: --rmi local entfernt nur Images, die direkt einem Compose-Service zugeordnet sind (via image:-Feld). Der builder-Stage ist ein anonymer Build-Cache-Eintrag des Classic Builders — kein Service-Image.

Loesung: Nach dem Build den Builder-Cache explizit praegen via docker builder prune -f (podman builder prune -f).

Dateiaenderungen

Datei Aktion Beschreibung
scripts/container.mjs Neu pruneBuildCache() exportieren (ruft CMD builder prune -f)
scripts/build.mjs Neu --prune-Flag: ruft pruneBuildCache() nach allen Builds
package.json Geaendert dev-Script: docker builder prune -f vor docker compose up

pruneBuildCache() in scripts/container.mjs

export function pruneBuildCache() {
  console.log(`${CYAN}Praeger Build-Cache...${RESET}`);
  execSync(`${CMD} builder prune -f`, { stdio: 'inherit' });
  console.log(`${GREEN}Build-Cache bereinigt.${RESET}`);
}

--prune in scripts/build.mjs

if (flags.prune) {
  pruneBuildCache();
}

Hinweis: In CI/CD-Umgebungen (Issue #212) wird docker builder prune -f separat im Cleanup-Step aufgerufen.

(Vorher irrtuemlich als neues Issue #215 angelegt — hier der korrekte Kommentar zu #206)

## Ergaenzung: Builder-Intermediate-Image wird nicht aufgeraeumt **Beobachtung:** Nach jedem `docker compose down -v --rmi local` bleibt ein unbenannter Intermediate-Image der `builder`-Stage aus dem Multi-Stage-Dockerfile (`backend/Dockerfile`) zurueck. **Ursache:** `--rmi local` entfernt nur Images, die direkt einem Compose-Service zugeordnet sind (via `image:`-Feld). Der `builder`-Stage ist ein anonymer Build-Cache-Eintrag des Classic Builders — kein Service-Image. **Loesung:** Nach dem Build den Builder-Cache explizit praegen via `docker builder prune -f` (`podman builder prune -f`). ### Dateiaenderungen | Datei | Aktion | Beschreibung | |-------|--------|-------------| | `scripts/container.mjs` | Neu | `pruneBuildCache()` exportieren (ruft `CMD builder prune -f`) | | `scripts/build.mjs` | Neu | `--prune`-Flag: ruft `pruneBuildCache()` nach allen Builds | | `package.json` | Geaendert | `dev`-Script: `docker builder prune -f` vor `docker compose up` | ### `pruneBuildCache()` in `scripts/container.mjs` ```javascript export function pruneBuildCache() { console.log(`${CYAN}Praeger Build-Cache...${RESET}`); execSync(`${CMD} builder prune -f`, { stdio: 'inherit' }); console.log(`${GREEN}Build-Cache bereinigt.${RESET}`); } ``` ### `--prune` in `scripts/build.mjs` ```javascript if (flags.prune) { pruneBuildCache(); } ``` **Hinweis:** In CI/CD-Umgebungen (Issue #212) wird `docker builder prune -f` separat im Cleanup-Step aufgerufen. > (Vorher irrtuemlich als neues Issue #215 angelegt — hier der korrekte Kommentar zu #206)
Sign in to join this conversation.
No description provided.