From 9d63377fda91b7a5278a11fc90cb4571b2429dd7 Mon Sep 17 00:00:00 2001 From: vch-demo Date: Wed, 2 Sep 2026 20:44:16 +0000 Subject: [PATCH] Initial commit --- .vch-description | 1 + .vch/PLATFORM.md | 326 +++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 29 +++++ README.md | 5 + package.json | 8 ++ server.js | 16 +++ 6 files changed, 385 insertions(+) create mode 100644 .vch-description create mode 100644 .vch/PLATFORM.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 package.json create mode 100644 server.js diff --git a/.vch-description b/.vch-description new file mode 100644 index 0000000..ee2322c --- /dev/null +++ b/.vch-description @@ -0,0 +1 @@ +Beispielprojekt der VCH-Hilfe. Zeigt einen vollstaendigen Deploy-Durchlauf. diff --git a/.vch/PLATFORM.md b/.vch/PLATFORM.md new file mode 100644 index 0000000..fd642e0 --- /dev/null +++ b/.vch/PLATFORM.md @@ -0,0 +1,326 @@ +# VCH-Plattform — die Regeln dieser Umgebung + +Diese Datei gehört VCH und wird bei jedem Abgleich **überschrieben**. Projektwissen gehört +in die `CLAUDE.md` daneben, nicht hierher. + +Sie beschreibt, was VCH mit diesem Projekt macht — vor allem die Stellen, an denen ein +Verstoß **nicht als Fehler auffällt, sondern als falsches Ergebnis**: ein Deploy, der grün +meldet und nichts ausliefert, eine Datenbank, die niemand verbindet, ein Audit, das etwas +prüft, das du gar nicht gepusht hast. + +## 1. Zwei Maschinen, nicht eine + +- **Hier (DEV):** ein Debian-LXC. Nutzer `vchuser`, Heimat `/home/vchuser`, Projekte unter + `/home/vchuser/projects/`. code-server auf Port 8080, Claude Code als `claude`, + Git gegen Gitea vorkonfiguriert. +- **Dort (PROD):** eine **andere** Maschine. VCH klont dein Repository dorthin und startet es. + +Daraus folgt die wichtigste Regel überhaupt: **VCH deployt und auditiert deinen gepushten +Commit, nie dein Arbeitsverzeichnis.** Nicht committete Änderungen existieren für Deploy, +Audit und Veröffentlichung nicht — auch dann nicht, wenn hier alles läuft. + +## 2. Vorschau im Browser läuft über einen Proxy + +Deine Entwicklungs-Server sind nicht direkt erreichbar. VCH erkennt lauschende Ports und +stellt zwei Wege bereit: + +1. **Subdomain** (bevorzugt): `https://lxc-./` — deine App liegt auf der + Wurzel, alles verhält sich normal. +2. **Pfad-Proxy** (Rückfallweg): `https:///proxy//p//` — deine App liegt + hinter einem Pfadpräfix. + +Damit beides funktioniert: **relative Pfade verwenden.** + +- `css/style.css` ✅ — nicht `/css/style.css` ❌ +- `fetch('api/daten')` ✅ — nicht `fetch('/api/daten')` ❌ +- Wenn absolute Pfade unvermeidbar sind: `` in den ``. + +Rahmenwerke: **Vite** `base: './'` · **Create React App** `"homepage": "."` in der +`package.json` · **Next.js** Vorgabe belassen (Subdomain) oder `basePath` setzen (Pfad-Proxy) +· **Express** `express.static` genügt, solange das HTML relativ verweist. + +## 3. Dateien, die VCH im Projektstamm liest + +| Datei | Pflicht | Wozu | +|---|---|---| +| `README.md` | ja | Audit-Prüfung `readme` — blockiert die Veröffentlichung | +| `.vch-description` | ja | Audit-Prüfung `description`; der Text steht später im Showcase | +| `.env.example` | wenn die App Umgebungsvariablen liest | die **Deklaration** aller Variablen (Abschnitt 6) | +| `.vch/deploy.yaml` | wenn die Autoerkennung nicht reicht | wie gebaut und gestartet wird (Abschnitt 5) | +| `CLAUDE.md` | — | dein Projektwissen; VCH fasst sie nicht mehr an | + +`README.md` muss beschreiben, **was** das Projekt tut, wie man es installiert, wie man es +startet und worauf es aufbaut. Die unveränderte Vorlage besteht die Prüfung nicht. + +`.vch-description` ist **nicht technisch**: was das Projekt tut, welches Problem es löst, +für wen. Kein Technologie-Stapel, keine Installationsanleitung — die stehen im README. +Halte beide aktuell, wenn sich Zweck oder Aufbau ändern. + +## 4. Der Port gehört VCH + +VCH weist jedem Projekt auf der PROD-Maschine einen festen Port zu (Bereich `13000–13999`) +und hält ihn über alle Deploys hinweg stabil. Du wählst ihn nicht und stimmst dich mit +niemandem ab. + +**Deine App MUSS auf `PORT` aus der Umgebung lauschen:** + +- Node/Express: `app.listen(process.env.PORT || 3000)` +- Next.js: `next start` beachtet `PORT` von selbst +- Python: `gunicorn app:app -b 0.0.0.0:$PORT` bzw. `uvicorn main:app --port $PORT` +- Go/andere: den Wert aus der Umgebung lesen, nicht hartkodieren + +Ein fest verdrahteter Port läuft hier und scheitert dort am Healthcheck. `PORT` und +`VCH_DATA_DIR` sind **VCH-eigene** Schlüssel: setzt du sie selbst — in `.env.example`, im +Env-Editor oder im Compose — wird dein Wert verworfen. + +## 5. `.vch/deploy.yaml` — die vollständige Referenz + +Ohne diese Datei versucht VCH, das Projekt selbst zu erkennen (Abschnitt 5.3). Reicht das +nicht, bricht der Deploy ab **und legt dir eine fertige Vorlage vor**, die du nur noch +committen musst. + +### 5.1 Alle Felder + +| Feld | Typ | Vorgabe | Bedeutung | +|---|---|---|---| +| `runtime` | `auto` \| `systemd` \| `docker` | `auto` | Wie gestartet wird. `auto`: Dockerfile oder Compose vorhanden → `docker`, sonst `systemd`. | +| `build` | Zeichenkette | — | Shell-Befehl im Release-Verzeichnis. **Nur `systemd`** — bei `docker` baut das Dockerfile. | +| `start` | Zeichenkette | — | Startbefehl des Dienstes. **Nur `systemd`.** Muss `$PORT` binden. | +| `port` | Ganzzahl 1–65535 | `3000` | **Nur `docker`:** der Port, auf dem deine App **im Container** lauscht. VCH bildet den zugewiesenen Host-Port darauf ab. | +| `health` | Pfad | `/` | Pfad, der 2xx oder 3xx liefert, sobald die App bereit ist. | +| `env` | Liste von Zeichenketten | `[]` | Zusätzliche Variablennamen, die VCH ausliefern soll. Wird mit den Schlüsseln aus `.env.example` vereinigt. | +| `compose` | Pfad | — | Welche Compose-Datei gilt, wenn mehrere gefunden werden. | + +Unbekannte Felder und Tippfehler in `runtime` brechen den Deploy mit einer benannten +Fehlermeldung ab — sie werden nicht still ignoriert. + +### 5.2 Beispiele + +Node mit eigenem Build (systemd): + +```yaml +runtime: systemd +build: "npm ci && npm run build" +start: "npm start" +health: / +``` + +Python (systemd) — beachte `$PORT` im Startbefehl: + +```yaml +runtime: systemd +build: "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt" +start: ".venv/bin/gunicorn app:app -b 0.0.0.0:$PORT" +health: / +``` + +Statische Seite (systemd) — irgendetwas muss lauschen: + +```yaml +runtime: systemd +build: "npm ci && npm run build" +start: "python3 -m http.server $PORT --directory dist" +``` + +Monorepo, Code in einem Unterordner: + +```yaml +runtime: systemd +build: "cd backend && npm ci && npm run build" +start: "cd backend && npm start" +``` + +Dockerfile: + +```yaml +runtime: docker +port: 3000 # der Port INNERHALB des Containers +health: / +``` + +Mehrere Compose-Dateien im Repository: + +```yaml +runtime: docker +compose: docker/compose.yaml +``` + +### 5.3 Was ohne die Datei passiert + +In dieser Reihenfolge: + +1. Ein echtes `Dockerfile` gewinnt → `runtime: docker` mit dem Image. +2. Eine Compose-Datei, die `${VCH_PORT}` veröffentlicht → Compose-Deploy. +3. Eine Compose-Datei, die **nur Infrastruktur** startet (Postgres, Redis, … und keinen + App-Dienst), wird als Entwicklungs-Beiwerk erkannt und **nicht** deployt — VCH sucht + weiter nach der Sprache. +4. Eine Compose-Datei mit App-Dienst, die `${VCH_PORT}` **nicht** veröffentlicht → Abbruch + mit Vorlage (Abschnitt 10). +5. `package.json` im Stamm: `build`-Skript → Build, `start`-Skript bzw. `main` → Start. + Paketmanager wird am Lockfile erkannt (`pnpm`/`yarn`/`npm`). +6. Alles andere — Python, PHP/Laravel, Go, reines Frontend ohne `start`, Code nur in + Unterordnern — endet mit einer passenden Vorlage zum Committen. + +## 6. Umgebungsvariablen: `.env.example` ist die Deklaration + +VCH liest keine Umgebungsvariablen aus deinem Code. **Was in einer Beispieldatei steht, +existiert; alles andere nicht.** + +- Erkannte Dateinamen: `.env.example`, `.env.sample`, `.env.template` sowie dieselben ohne + führenden Punkt (`env.example`, …). Der erste Fund je Schlüssel gewinnt. +- Gesucht wird **im ganzen Repository** (ohne `node_modules`, `.git`, `dist`, `build`, + `.next`, `out`, `vendor`, `coverage`, `.turbo`), Stammdateien zuerst, höchstens 25 Dateien. +- Jede gefundene Beispieldatei bestimmt ein Ziel: `backend/.env.example` → VCH schreibt die + Produktionswerte nach `backend/.env`. Für Monorepos ist das der Weg, mehrere `.env` zu + bekommen. + +**Was den Deploy blockiert:** ein Schlüssel, der **keinen Wert nennt** (`KEY=`) und für den +noch kein Produktionswert hinterlegt ist. Nicht blockierend sind: + +```dotenv +API_URL=https://beispiel.test # Vorgabewert vorhanden +OPTIONAL_TOKEN="" # ausdrücklich leer erlaubt +FEATURE_FLAG= # optional +``` + +Der Marker muss direkt hinter dem `#` stehen (`# optional`); Fließtext, in dem das Wort +vorkommt, zählt nicht. Die Vorgabewerte sind **Hinweise für den Menschen** — in Produktion +wird nichts davon verwendet. + +Im Zweifel: + +```bash +grep -RhoE "process\.env\.[A-Z_]+" src/ | sort -u +``` + +und jeden Treffer in die `.env.example` schreiben. + +**Bei Docker:** nur Variablen mit dem Präfix `NEXT_PUBLIC_`, `VITE_` oder `PUBLIC_` gehen als +`--build-arg` in den Build (sie sind ohnehin im Auslieferungsstand sichtbar) — dafür brauchst +du passende `ARG`-Zeilen im Dockerfile. Alle übrigen Werte kommen erst zur Laufzeit über +`--env-file` und landen nie im Image. + +## 7. Datenbank + +Deklariert deine App Verbindungsvariablen, bietet VCH eine verwaltete Datenbank an und trägt +die Werte **automatisch** in genau die Schlüssel ein, die du deklariert hast. Du musst deine +Variablennamen also nicht ändern — VCH erkennt sie. + +**Erkannt wird** (Groß-/Kleinschreibung egal): + +- Verbindungszeichenketten: `DATABASE_URL`, `DB_URL`, jeder Name auf `_DSN` +- Einzelteile mit Familienwort (`DB`, `DATABASE`, `POSTGRES`, `MYSQL`) plus Rolle: + `..._HOST`, `..._PORT`, `..._USER`/`..._USERNAME`, `..._PASSWORD`/`..._PASS`, + `..._NAME`/`..._DATABASE`/`..._DB` +- Die üblichen nackten Namen: `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, + `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `POSTGRES_DB` + +**Bewusst nicht erkannt** (und deshalb nie überschrieben): + +- alles mit `ROOT` im Namen (`POSTGRES_ROOT_PASSWORD`, `MYSQL_ROOT_PASSWORD`) — das sind die + Zugangsdaten des Datenbankservers, nicht die deiner App +- `SENTRY_DSN` und verwandte Fehlermelder (`GLITCHTIP`, `BUGSNAG`, `ROLLBAR`, `AIRBRAKE`, + `RAYGUN`, `APPSIGNAL`) — ein DSN ist dort keine Datenbank +- `HISTORY_DB=/data/verlauf.sqlite` und Ähnliches: ein `..._DB` ohne Datenbank-Familienwort + im übrigen Namen gilt nicht als Verbindung + +Die **Engine** (Postgres oder MySQL) leitet VCH aus deinen Schlüsselnamen und dem Schema in +`DATABASE_URL` ab; ist das mehrdeutig, entscheidest du im Projektarbeitsplatz. + +**Bringt dein Compose seine eigene Datenbank mit**, startet VCH sie mit und bietet **keine** +zweite an. Achte dann selbst auf ein dauerhaftes Volume — ein Datenbankdienst ohne Volume +verliert seine Daten beim nächsten Neubau, und VCH warnt nur. + +Eine Compose-Datei, die **nur** eine Datenbank startet, ist Entwicklungs-Beiwerk: sie läuft in +Produktion nicht mit. Nimm dort die verwaltete Datenbank. + +## 8. Dauerhafte Daten: `VCH_DATA_DIR` + +Jedes Release liegt in einem eigenen Verzeichnis und verschwindet wieder. Was überleben soll — +SQLite-Datei, Uploads, erzeugte Dateien — gehört in das Verzeichnis aus **`VCH_DATA_DIR`**. + +```js +const datenVerzeichnis = process.env.VCH_DATA_DIR || './data'; +``` + +VCH setzt die Variable bei allen Betriebsarten (bei Docker als Einhängepunkt `/data`). **Lies +sie zur Laufzeit**, schreibe den Pfad nirgends fest und trage sie nicht in `.env`-Dateien ein. +Ein Verzeichnis relativ zum Release (`./data`) ist beim nächsten Deploy weg. + +Der Prüfling während des Audits (Abschnitt 9) bekommt diesen Speicher **nicht** — er läuft +gegen einen leeren. Das ist Absicht: zwei Prozesse dürfen nicht gleichzeitig auf dieselbe +SQLite-Datei schreiben. + +## 9. Wie ein Deploy abläuft + +``` +Klonen → Bauen → Healthcheck (Prüfling) → Audit → Umschalten → Live +``` + +1. **Klonen.** VCH klont den Deploy-Branch auf die PROD-Maschine — genau den gepushten Stand. +2. **Torprüfung.** Fehlt ein blockierender Wert aus `.env.example` (Abschnitt 6), endet es hier, + bevor irgendetwas läuft. +3. **Bauen** nach `.vch/deploy.yaml` bzw. Autoerkennung. +4. **Healthcheck.** Der neue Stand startet als **Prüfling** auf einem freien Port aus dem + Bereich `14000–14999`, neben der weiterhin laufenden alten Fassung. VCH ruft dort deinen + `health`-Pfad auf: rund 30 Sekunden lang, alle 1,5 s ein Versuch, Erfolg beim ersten + 2xx/3xx. Bis dahin muss die App antworten. +5. **Audit** (Abschnitt 10) gegen den deployten Commit. +6. **Umschalten.** Erst wenn das Audit besteht, wird der Prüfling zur laufenden Fassung auf + dem stabilen Port. **Ein durchgefallenes Audit heißt: der alte Stand bleibt live.** + +Vor dem Umschalten legt VCH einen Datenschnappschuss an. Geht das Umschalten schief, werden +erst die Daten und dann der Code zurückgerollt. Frühere Releases lassen sich im +Projektarbeitsplatz per Rollback zurückholen. + +**Auto-Deploy:** ist er eingeschaltet, löst jeder Push auf den konfigurierten Deploy-Branch +einen Deploy aus. Pushes auf andere Branches werden ignoriert. + +## 10. Audit — was geprüft wird + +Das Audit läuft nicht hier, sondern in einem eigenen Runner gegen einen **sauberen Checkout +des deployten Commits**. Nicht Gepushtes wird nicht geprüft — und ein `.gitignore`-Eintrag +macht eine Datei für das Audit unsichtbar. + +| Prüfung | Schwere | Darf entfallen | +|---|---|---| +| `readme` | Fehler | nein | +| `secrets` | Fehler | nein | +| `description` | Warnung | nein | +| `lint` | Fehler | ja | +| `typescript` | Fehler | ja | +| `npm-audit` | Fehler | ja | + +- Eine **Warnung** blockiert nie. `description` hält dich also nicht auf, kostet dich aber den + Platz im Showcase. +- „Darf entfallen" heißt: die Prüfung meldet `skipped`, wenn sie auf dein Projekt nicht passt + (kein TypeScript, kein npm). Eine Prüfung, die **nicht** entfallen darf, aber trotzdem + `skipped` meldet, gilt als **durchgefallen** — sie ist nie gelaufen. +- `secrets` sucht eingecheckte Zugangsdaten. Echte Werte gehören nie ins Repository: in die + `.env.example` nur Beispiele, die echten Werte in den Env-Editor von VCH. + +## 11. Veröffentlichen + +- **Intern erreichbar** ist dein Projekt, sobald ein Deploy läuft — ohne Audit, ohne weiteren + Schritt, unter `http://:`. +- **Öffentlich** unter einer Subdomain mit SSL wird es erst mit laufendem Deploy **und** + bestandenem Audit für genau diesen Deploy. Die Subdomain kommt oben drauf; entfernst du sie + wieder, bleibt das Projekt intern erreichbar. +- Im **Showcase** erscheinen veröffentlichte Projekte mit bestandenem Audit und einer + eingecheckten `.vch-description`. + +## 12. Compose — die vier Regeln, an denen es sonst scheitert + +1. **Der App-Dienst muss seinen Port als `ports: ["${VCH_PORT}:"]` veröffentlichen.** + VCH setzt `VCH_PORT` und kann so einen Prüfling neben deiner laufenden Fassung starten. + Ohne diese Zeile bricht der Deploy ab. +2. **VCH führt genau EINE Compose-Datei aus — keine `-f`-Ketten.** Gesucht wird + `compose.yaml`/`compose.yml`/`docker-compose.yaml`/`docker-compose.yml` im Stamm **oder in + einem Unterordner**. Eine Override-Datei (`docker-compose.ports.yml`, `*.override.yml`) + wird **ignoriert** — die `${VCH_PORT}`-Zeile gehört in die Hauptdatei. Bei mehreren + Fundstellen: `compose: ` in die `.vch/deploy.yaml`. +3. **Jeder Dienst, der Produktionswerte braucht, bekommt `env_file: [.env]`.** VCH schreibt + deine Werte in eine `.env` neben der Compose-Datei. +4. **Der ganze Stapel startet.** Datenbanken, Caches und Ähnliches aus deinem Compose laufen + automatisch mit — eine zusätzliche verwaltete Datenbank brauchst du dafür nicht. Dienste + hinter einem `profiles:`-Schlüssel bleiben aus. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc42de3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +@.vch/PLATFORM.md + + + +# beispiel-app + +Wofür dieses Projekt gut ist, und was jemand wissen muss, der hier weiterarbeitet. + +## Befehle + +- **Abhängigkeiten installieren:** `npm install` +- **Entwicklungsserver:** `npm run dev` +- **Tests:** `npm test` +- **Bauen:** `npm run build` +- **Linten:** `npm run lint` + +## Konventionen + +- Aussagekräftige Commit-Nachrichten im Imperativ („Suche ergänzen", nicht „Suche ergänzt") +- Lieber kleine, abgeschlossene Commits als große +- Vor dem Commit linten und formatieren +- Tests für die tragende Fachlogik +- Wenig Abhängigkeiten — was Browser und Node schon können, reicht meistens +- Semantisches HTML, mobil zuerst, bedienbar per Tastatur, ausreichender Kontrast +- Konfiguration über Umgebungsvariablen; keine Zugangsdaten im Quelltext diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f90687 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Beispiel-App + +Ein bewusst winziges Node-Projekt. Es existiert, damit die Bilder der VCH-Hilfeseite +einen echten Deploy, eine Pipeline und einen Verlauf zeigen koennen ��� nicht als Vorlage +fuer eigene Projekte. diff --git a/package.json b/package.json new file mode 100644 index 0000000..403e7ba --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "beispiel-app", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node server.js" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..b520423 --- /dev/null +++ b/server.js @@ -0,0 +1,16 @@ +// Beispielanwendung fuer die Hilfebilder: haelt keine Daten, braucht keine Datenbank. +const http = require('http'); +const port = process.env.PORT || 3000; + +http + .createServer((req, res) => { + if (req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + return; + } + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end('Beispiel-App' + + '

Beispiel-App

Diese Seite dient den Bildern der VCH-Hilfe.

'); + }) + .listen(port, () => console.log('listening on ' + port));