# Webhook-Migration: Standalone Listener → Nitro-Endpoint (Trucko-Pattern)

**Owner:** Backyard
**Erstellt:** 2026-05-07
**Status:** PLAN — keine Implementierung in dieser Session. User-Direktive: „langsam starten".
**Ziel:** Migration des GitHub-Webhook-Receivers vom standalone `scripts/deploy-webhook/server.mjs` zu einem Nitro-Endpoint im Backend (`backend/server/api/webhook/github.post.ts`) — analog zum Trucko-Monorepo.

---

## 1. Status quo (Stand 2026-05-07)

### 1.1 Aktuelle Pipeline

```
GitHub push → nginx (staging.rebreak.org/webhook)
            → 127.0.0.1:9000 (pm2: rebreak-webhook)
            → scripts/deploy-webhook/server.mjs (HMAC-Verify + Queue)
            → spawn bash scripts/deploy.sh
              → git fetch + reset --hard origin/main
              → pnpm install --frozen-lockfile (workspace-root)
              → cd backend && pnpm --filter rebreak-backend build
              → .output → .output-staging (atomisch via tmp)
              → pm2 restart rebreak-staging --update-env
              → pm2 restart rebreak-imap-staging / rebreak-idle-staging / dns-* (best-effort)
```

### 1.2 Beteiligte Dateien

| Datei | Rolle |
|------|-------|
| `/srv/rebreak/scripts/deploy-webhook/server.mjs` | Standalone Node-HTTP-Server auf Port 9000, HMAC-Verify, spawn deploy.sh |
| `/srv/rebreak/scripts/deploy.sh` | Pull → install → build → atomic-deploy → pm2 restart |
| `/srv/rebreak/ecosystem.config.js` | pm2-Service `rebreak-webhook` (cluster mode, 1 instance) |
| `/etc/nginx/sites-enabled/staging.rebreak.org` | `location /webhook` → `proxy_pass http://127.0.0.1:9000/webhook` |
| `/etc/environment` | `GITHUB_WEBHOOK_SECRET` (via Infisical-bootstrap), `INFISICAL_CLIENT_ID/SECRET` |

### 1.3 Beobachtete Probleme

1. **Push-Detection unzuverlässig:** Manche Pushes triggern Deploy nicht (Listener empfängt Event nicht oder schweigt) — Symptom unklar (timing/network/hängender vorheriger spawn?).
2. **deploy.sh exit code 1:** Vergangene Logs zeigen `cd apps/rebreak: No such file or directory` (alter Pre-Cutover-Pfad in alten Logs; aktueller deploy.sh ist auf `backend/` korrekt umgestellt — Symptom nicht mehr reproduzierbar, aber Vertrauensverlust).
3. **Kein Healthcheck:** `/webhook` antwortet nur auf POST mit valider Sig. Kein `GET /webhook/health` o.ä. — manuelles Debug nur via `pm2 logs rebreak-webhook`.
4. **Kein Retry:** Wenn `git fetch` netzwerkbedingt failt, bleibt Deploy hängen / aborts ohne Rescheduling.
5. **Logs verstreut:** Webhook-Logs in `pm2 logs rebreak-webhook`, Deploy-Output in dieselben Logs gestreamt aber mit `[deploy] /[deploy:err]`-Prefixen — kein strukturiertes Logging, kein File-Persistence über pm2-Restart hinweg.
6. **Single-Point-of-Failure:** Wenn `rebreak-webhook` pm2-Prozess crashed, gehen Pushes ins Leere — keine GitHub-Redelivery-Automatik (manuell via GitHub UI nötig).

---

## 2. Trucko-Pattern (das Vorbild)

**Quelldatei:** `~/mono/trucko-monorepo/backend/server/api/webhook/github.post.ts` (957 Zeilen)
**Test-Coverage:** `~/mono/trucko-monorepo/backend/tests/unit/webhook-hmac.test.ts` (HMAC-Verify isoliert testbar)

### 2.1 Architektur-Eigenschaften

- **Webhook ist ein Nitro-API-Endpoint im Backend selbst** — kein separater Listener.
  Pfad: `backend/server/api/webhook/github.post.ts` → wird über das Backend-pm2 (`rebreak-staging`) bedient.
- **HMAC-Sig-Verify** identisch wie unsere `server.mjs` (sha256, `timingSafeEqual`).
- **Reads `useRuntimeConfig()`** für `GITHUB_WEBHOOK_SECRET`, `GITHUB_USERNAME`, `GITHUB_TOKEN` — Secrets kommen via Infisical → Nuxt/Nitro runtimeConfig (kein extra `/etc/environment`-Parsing).
- **Affected-Detection:** Liest `payload.commits[].added/modified/removed`, normalisiert Pfade, mapped auf eine Tabelle `affected = { rebreak: bool, backend: bool, nginx: bool, ... }`. Nur betroffene Apps werden gebaut.
- **Queue-Mechanismus mit Merge:** `queueDeployment()` mergt `affected`-Flags wenn ein zweiter Push während eines laufenden Builds reinkommt → kein Doppel-Build, aber kein Verlust.
- **Inline-Build-Steps via `executeCommand()`-Helper:** Jeder Step (git fetch / install / build / pm2 restart) ist ein einzelner spawned bash mit Timeout + structured stdout/stderr-Capture. Kein externes deploy.sh.
- **Token-basierter Pull:** Nutzt `GITHUB_USERNAME` + `GITHUB_TOKEN` für `https://...@github.com/...` — kein SSH-Deploy-Key-Handling im Webhook.
- **Atomisches Output-Swap** für rebreak: `cp -r .output .output-staging-new && rm -rf .output-staging && mv .output-staging-new .output-staging` (identisch zu unserem deploy.sh).
- **Logs in pm2-stream:** Alles via `console.log("[Webhook] ...")` — landet in `pm2 logs backend`, zentral.
- **Healthcheck implizit:** Wenn das Backend lebt, lebt der Webhook. pm2 zeigt `backend online` → Webhook funktioniert.

### 2.2 Was das Trucko-Pattern besser macht

| Aspekt | Standalone-Listener (Status quo) | Nitro-Endpoint (Trucko) |
|--------|-----------------------------------|-------------------------|
| Single Source of Truth | 2 pm2-Services (webhook + app) | 1 pm2-Service (Backend allein) |
| Secret-Handling | `/etc/environment` parsing manuell | `useRuntimeConfig()` via Infisical |
| Affected-Logik | Nicht vorhanden, baut immer alles | Granular, nur betroffene Apps |
| Logs | 2 verschiedene pm2-Streams | 1 zentraler Stream |
| Test-Coverage | Keine Tests | `webhook-hmac.test.ts` (vitest) |
| Code-Lokation | `scripts/` (separater Code-Pfad) | `backend/server/api/` (gleiche TS-Codebase) |
| Crash-Resilience | Eigener Prozess kann sterben ohne app | Wenn Backend lebt, lebt Webhook |

---

## 3. Migrations-Schritte (für nächste Session — NICHT jetzt)

### Phase 1: Endpoint anlegen (Mac, kein Server-Change)

1. Datei `backend/server/api/webhook/github.post.ts` erstellen — kopiere Trucko-Vorlage, passe an:
   - Repo-Pfad: `/srv/rebreak` (statt `/srv/trucko-monorepo`)
   - `affected`-Map: nur `rebreak-native`, `backend`, `nginx`, `dns`, `imap` (kein `pizzabox`/`driver`/etc.)
   - Build-Steps: nur Backend-Build (`pnpm --filter rebreak-backend build`) + atomic-output-swap + pm2 restart `rebreak-staging`
   - Entferne Trucko-spezifische nginx-conf-Listen (die unsere ops/nginx-Files haben andere Namen)
2. `nitro.config.ts`: `runtimeConfig` um `githubWebhookSecret`, `githubUsername`, `githubToken` ergänzen (analog Trucko).
3. Test-File `backend/tests/unit/webhook-hmac.test.ts` mitmigrieren (vitest setup falls nötig).
4. Lokal `pnpm --filter rebreak-backend build` → checken dass Endpoint im `.output/server/chunks/routes/api/webhook/github.post.mjs` landet.

### Phase 2: Infisical-Secrets setzen (User-Eskalation)

5. Infisical Project rebreak-staging:
   - `GITHUB_WEBHOOK_SECRET` (bereits vorhanden in /etc/environment, in Infisical spiegeln)
   - `GITHUB_USERNAME` (z.B. `chahinebrini`)
   - `GITHUB_TOKEN` (PAT mit `repo`-Scope für git-pull über HTTPS)
   → diese Secrets werden via `start-staging.sh` als `NUXT_*` env vars dem Backend bereitgestellt.

### Phase 3: Parallel-Betrieb (Failsafe)

6. **Beide Endpoints aktiv halten:**
   - GitHub-Webhook bleibt auf `https://staging.rebreak.org/webhook` (alter Listener auf Port 9000).
   - Neuer Endpoint zusätzlich erreichbar unter `https://staging.rebreak.org/api/webhook/github` (Backend-Routing).
7. Auf GitHub: **zweite Webhook-Konfig** anlegen, beide aktiv. Beide verifizieren mit demselben Secret.
8. Logs beide Streams beobachten — wenn neuer Endpoint zuverlässig deployt, alter wird redundant.

### Phase 4: Cutover

9. GitHub-Webhook umstellen: alten URL-Webhook deaktivieren, nur neuer `/api/webhook/github` aktiv.
10. nginx: `location /webhook` → `proxy_pass http://127.0.0.1:9000` ENTFERNEN (oder auf Backend umrouten).
11. `pm2 stop rebreak-webhook && pm2 delete rebreak-webhook` (NUR auf User-Approval — destruktiv).
12. `scripts/deploy-webhook/` archivieren (nicht löschen — Reference).
13. `scripts/deploy.sh` entweder:
    - **a)** behalten als Fallback-Tool für manuelles `bash deploy.sh` auf SSH (empfohlen), oder
    - **b)** Code-Logik ins Backend-Endpoint inline-spawnen wie Trucko es tut (mehr Code, aber 100% Single-Source).

### Phase 5: Hardening

14. Healthcheck-Endpoint hinzufügen: `backend/server/api/webhook/health.get.ts` → returns `{ ok, lastDeploy, queueDepth }`.
15. Persistent Deploy-Log: in `/var/log/rebreak/deploys.jsonl` schreiben (statt nur pm2-stream).
16. Slack/Discord-Notifier on deploy-fail (optional).

---

## 4. Risk-Assessment

### Top 3 Risks bei Migration

1. **Self-Deploy-Loop / Race-Condition beim Restart:**
   Der Webhook lebt im Backend-Prozess. Wenn der Webhook `pm2 restart rebreak-staging` aufruft, killt der Prozess sich selbst, BEVOR die HTTP-Response gesendet ist. → GitHub bekommt timeout, retry-storm. **Mitigation:** Trucko macht's auch, und es funktioniert dort. Schlüssel: pm2 restart ist async + `setTimeout(2000)` vor dem restart, response wurde schon vor Build-Start gesendet (`return { ok }` BEVOR `queueDeployment()` läuft im Background). Muss bei uns **gleich** strukturiert werden.

2. **Build-Failure crashed Webhook-Capability:**
   Wenn ein Bad-Push das Backend-Build kaputt macht, hat der nächste Push keinen Webhook-Endpoint mehr (Backend offline). → Stuck-State, manuelles SSH nötig zum Recovery.
   **Mitigation:** Atomic-Output-Swap (.output → .output-staging via mv) bleibt. Alter Build überlebt im `.output-staging`, Backend bleibt online auch wenn neuer Build failed. **PLUS:** Listener als Failsafe parallel erstmal behalten (Phase 3).

3. **Infisical-Secret-Loading-Lücke:**
   Wenn `useRuntimeConfig()` zur Build-Zeit aufgerufen wird (statt Runtime), landen die Secrets nicht im Endpoint. Trucko nutzt `runtimeConfig` → wird per `NUXT_*`-env bei Start aufgelöst. Unsere `start-staging.sh` muss `NUXT_GITHUB_WEBHOOK_SECRET`, `NUXT_GITHUB_USERNAME`, `NUXT_GITHUB_TOKEN` setzen — sonst 401 für jeden Push.
   **Mitigation:** Phase 2 explizit verifizieren via `curl https://staging.rebreak.org/api/webhook/github -X POST -H "x-hub-signature-256: sha256=..." -d '...'` mit Test-Payload BEVOR GitHub-Webhook umgestellt wird.

### Weitere Risiken (lower-priority)

- **GitHub-Token-Lifetime:** PAT läuft ab → silent fail. Mitigation: Token mit langer Lifetime, ggf. Rotation-Reminder.
- **Body-Parsing-Reihenfolge:** Nitro liest body normal als JSON, aber HMAC braucht raw-body. Trucko liest manuell `event.req.on("data")` — Nitro-Middleware darf body nicht vorher konsumiert haben. Ggf. Plugin-Order checken.
- **OOM während Build:** 4 GB RAM, `NODE_OPTIONS=--max-old-space-size=1536` reicht heute. Wenn Backend-Build wächst → OOM-kill mid-build → nicht-atomar.

---

## 5. Fallback-Plan

Wenn Migration scheitert:

1. **Listener bleibt aktiv** während Migration (Phase 3 ist parallel-Betrieb).
2. **Rollback-Path:**
   - GitHub-Webhook URL zurück auf `/webhook` (alter Listener).
   - `pm2 restart rebreak-webhook` (falls gestoppt).
   - Neuer Endpoint im Backend bleibt vorhanden, antwortet aber nicht mehr aus GitHub-Pushes — kein Schaden.
3. **Total-Rollback (nur falls Backend-Endpoint Backend-Builds blockiert):**
   - File `backend/server/api/webhook/github.post.ts` löschen, neu builden, deployen.
   - Cleanup: `runtimeConfig`-Einträge in `nitro.config.ts` zurückrollen.

---

## 6. Empfohlener erster Schritt (für nächste Session)

**Read-only Investigation:** Prüfen welche Infisical-Secrets aktuell im Backend-Runtime verfügbar sind (`pm2 logs rebreak-staging | grep -i webhook` + Infisical-CLI `infisical secrets --env=staging | grep GITHUB`). Damit ist klar ob Phase 2 trivial ist (Secrets bereits da) oder neue Secrets nötig sind. **Erst danach** Phase 1 (Endpoint anlegen) auf Mac, ohne deployen.

---

## 7. Was NICHT in Scope

- Code-Änderungen am App-Code (Nitro-Routes außerhalb `/webhook/`).
- Mail-Stack-Touchups (Mo's Scope).
- nginx-Routing-Änderung jetzt — erst nach Cutover-Phase 4.
- Force-Push / git history-rewrite.
