chore(cutover): prepare backend/-Layout for Hetzner-Pipeline-Cutover
Phase-1-Vorbereitung für den Rebreak-Cutover (apps/rebreak Nuxt → backend
standalone Nitro). Alle Änderungen sind lokal verifiziert (build = 9.66 MB
gzipped 3.08 MB, node .output/server/index.mjs startet ohne ERR_MODULE_NOT_FOUND
auf :3000). Kein Push, kein Server-Eingriff in dieser Session.
Inhalt:
- backend/nitro.config.ts: 8 zusätzliche runtimeConfig-Keys (cartesia*, eleven*,
supabaseUrl/AnonKey/ServiceKey, public.supabase.{url,key}). Schließt den
Auth-500-Cascade vom 2026-05-06 (server/utils/auth.ts:32 liest
config.public.supabase ?? config.supabase — beide Pfade jetzt deklariert).
- .npmrc (NEU, root-level): node-linker=hoisted für Prisma 7 transitive
@prisma/client-runtime-utils (siehe feedback_backend_runtime_config.md).
- backend/start-staging.sh: Pfad korrigiert von /srv/rebreak-monorepo/...
→ /srv/rebreak/backend/.output-staging/server/index.mjs. infisical run
wrapper (kein NUXT_*-Mapping mehr — runtimeConfig liest process.env.X
direkt). IMAP-Services entfernt (sind Mo's Scope, separat in ecosystem).
- scripts/deploy.sh (NEU): adaptiert von /srv/rebreak/scripts/deploy.sh
für backend/-Layout. APP_DIR=backend, pnpm --filter rebreak-backend build,
.output → .output-staging atomic-move bleibt erhalten, pm2 restart
--update-env zieht neue Infisical-Secrets.
- scripts/deploy-webhook/server.mjs (NEU): 1:1-Kopie vom Server, damit
ecosystem.config.js auf die Repo-Version zeigen kann.
- ecosystem.config.js (NEU, root-level): rebreak-staging zeigt auf
backend/start-staging.sh, rebreak-webhook zeigt auf scripts/deploy-webhook.
rebreak-prod + dns-* sind kommentiert (folgen in späterer Phase).
- ops/CUTOVER_PLAN.md: Plan-Doku vom 2026-05-06 (yesterday's work).
- .gitignore: .claude/ und xgit ergänzt (lokale Agent-State, nicht versioniert).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5dfbe886a4
commit
d1b71e76b2
6
.gitignore
vendored
6
.gitignore
vendored
@ -27,3 +27,9 @@ Thumbs.db
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Claude Code agent state (lokale Definitionen, nicht versioniert)
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# xgit binary (generated)
|
||||||
|
xgit
|
||||||
|
|||||||
@ -16,27 +16,65 @@ export default defineNitroConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
// ─── Database / Core ─────────────────────────────────────────────────
|
||||||
databaseUrl: process.env.DATABASE_URL ?? "",
|
databaseUrl: process.env.DATABASE_URL ?? "",
|
||||||
|
encryptionKey: process.env.ENCRYPTION_KEY ?? "",
|
||||||
|
|
||||||
|
// ─── Admin / Cron ────────────────────────────────────────────────────
|
||||||
adminSecret: process.env.ADMIN_SECRET ?? "",
|
adminSecret: process.env.ADMIN_SECRET ?? "",
|
||||||
|
cronSecret: process.env.CRON_SECRET ?? "",
|
||||||
|
|
||||||
|
// ─── LLM-Provider ────────────────────────────────────────────────────
|
||||||
openrouterApiKey: process.env.OPENROUTER_API_KEY ?? "",
|
openrouterApiKey: process.env.OPENROUTER_API_KEY ?? "",
|
||||||
deepgramApiKey: process.env.DEEPGRAM_API_KEY ?? "",
|
openaiApiKey: process.env.OPENAI_API_KEY ?? "",
|
||||||
googleApiKey: process.env.GOOGLE_API_KEY ?? "",
|
groqApiKey: process.env.GROQ_API_KEY ?? "",
|
||||||
googleAiApiKey: process.env.GOOGLE_AI_API_KEY ?? "",
|
googleAiApiKey: process.env.GOOGLE_AI_API_KEY ?? "",
|
||||||
|
|
||||||
|
// ─── TTS-Provider ────────────────────────────────────────────────────
|
||||||
|
googleApiKey: process.env.GOOGLE_API_KEY ?? "",
|
||||||
|
deepgramApiKey: process.env.DEEPGRAM_API_KEY ?? "",
|
||||||
azureTtsKey: process.env.AZURE_TTS_KEY ?? "",
|
azureTtsKey: process.env.AZURE_TTS_KEY ?? "",
|
||||||
azureTtsRegion: process.env.AZURE_TTS_REGION ?? "",
|
azureTtsRegion: process.env.AZURE_TTS_REGION ?? "",
|
||||||
openaiApiKey: process.env.OPENAI_API_KEY ?? "",
|
// NEU im backend/-Layout (existieren in nuxt.config.ts NICHT, aber backend code liest sie)
|
||||||
|
cartesiaApiKey: process.env.CARTESIA_API_KEY ?? "",
|
||||||
|
cartesiaVoiceId: process.env.CARTESIA_VOICE_ID ?? "",
|
||||||
|
elevenlabsApiKey: process.env.ELEVENLABS_API_KEY ?? "",
|
||||||
|
elevenlabsVoiceId: process.env.ELEVENLABS_VOICE_ID ?? "",
|
||||||
|
|
||||||
|
// ─── Supabase (Server-only) ──────────────────────────────────────────
|
||||||
|
// Im alten Nuxt-Layout via @nuxtjs/supabase auto-injected. In standalone Nitro
|
||||||
|
// explizit deklarieren, damit auth/middleware nicht 500't.
|
||||||
|
// server/utils/auth.ts:32 liest `config.public.supabase ?? config.supabase`,
|
||||||
|
// also beide Pfade müssen existieren.
|
||||||
|
supabaseUrl: process.env.SUPABASE_URL ?? "https://db-staging.rebreak.org",
|
||||||
|
supabaseAnonKey: process.env.SUPABASE_ANON_KEY ?? "",
|
||||||
|
supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
|
||||||
|
|
||||||
|
// ─── Stripe ──────────────────────────────────────────────────────────
|
||||||
stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "",
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "",
|
||||||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "",
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "",
|
||||||
|
|
||||||
|
// ─── Email / External APIs ───────────────────────────────────────────
|
||||||
resendApiKey: process.env.RESEND_API_KEY ?? "",
|
resendApiKey: process.env.RESEND_API_KEY ?? "",
|
||||||
encryptionKey: process.env.ENCRYPTION_KEY ?? "",
|
|
||||||
|
// ─── Bot-User-IDs (DB-User-References für Lyra/Rebreak-Bot-Posts) ────
|
||||||
lyraBotUserId: process.env.LYRA_BOT_USER_ID ?? "",
|
lyraBotUserId: process.env.LYRA_BOT_USER_ID ?? "",
|
||||||
rebreakBotUserId: process.env.REBREAK_BOT_USER_ID ?? "",
|
rebreakBotUserId: process.env.REBREAK_BOT_USER_ID ?? "",
|
||||||
groqApiKey: process.env.GROQ_API_KEY ?? "",
|
|
||||||
cronSecret: process.env.CRON_SECRET ?? "",
|
// ─── Public (client-readable) ────────────────────────────────────────
|
||||||
public: {
|
public: {
|
||||||
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY ?? "",
|
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY ?? "",
|
||||||
appUrl: process.env.APP_URL ?? "https://staging.rebreak.org",
|
appUrl: process.env.APP_URL ?? "https://staging.rebreak.org",
|
||||||
apiBase: process.env.API_BASE ?? "https://staging.rebreak.org",
|
apiBase: process.env.API_BASE ?? "https://staging.rebreak.org",
|
||||||
|
// server/utils/auth.ts liest config.public.supabase.{url,key}
|
||||||
|
// — wenn das fehlt, 500-cascade auf allen authentifizierten Routes
|
||||||
|
// (Incident 2026-05-06).
|
||||||
|
supabase: {
|
||||||
|
url:
|
||||||
|
process.env.SUPABASE_URL ??
|
||||||
|
"https://db-staging.rebreak.org",
|
||||||
|
key: process.env.SUPABASE_ANON_KEY ?? "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
37
backend/start-staging.sh
Normal file → Executable file
37
backend/start-staging.sh
Normal file → Executable file
@ -1,10 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# rebreak-backend Staging — startet Nitro mit Infisical-Secrets.
|
# rebreak-backend Staging — startet Nitro mit Infisical-Secrets.
|
||||||
# Pattern analog trucko-backend/start-prod.sh, aber env=staging.
|
#
|
||||||
|
# Pattern: infisical login (universal-auth) → infisical run (--env=staging)
|
||||||
|
# spritzt secrets als process.env.X in den node-Prozess.
|
||||||
|
# Nitro's runtimeConfig (siehe nitro.config.ts) liest sie direkt — kein
|
||||||
|
# NUXT_*-Prefix-Mapping mehr nötig (jeder Key in nitro.config.ts hat
|
||||||
|
# `process.env.X ?? ""` als Default).
|
||||||
|
#
|
||||||
|
# Pfad-Konvention (Backyard-Layout, post-cutover):
|
||||||
|
# - Repo-Root: /srv/rebreak
|
||||||
|
# - Backend-Dir: /srv/rebreak/backend
|
||||||
|
# - Build-Output (deployt von scripts/deploy.sh): backend/.output-staging/server/index.mjs
|
||||||
|
#
|
||||||
|
# IMAP-Services (rebreak-imap-staging, rebreak-idle-staging) sind NICHT mehr
|
||||||
|
# Teil dieses Scripts — sie werden separat über ecosystem.config.js verwaltet
|
||||||
|
# (Mo's Scope, fährt unter /srv/rebreak/apps/rebreak/imap-{proxy,idle}/).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
source /etc/environment
|
source /etc/environment
|
||||||
|
|
||||||
if [[ -z "$INFISICAL_CLIENT_ID" || -z "$INFISICAL_CLIENT_SECRET" ]]; then
|
if [[ -z "${INFISICAL_CLIENT_ID:-}" || -z "${INFISICAL_CLIENT_SECRET:-}" ]]; then
|
||||||
echo "❌ INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET nicht gesetzt in /etc/environment" >&2
|
echo "[start-staging] FEHLER: INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET nicht in /etc/environment" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -15,7 +32,7 @@ INFISICAL_TOKEN=$(infisical login \
|
|||||||
--silent --plain 2>/dev/null)
|
--silent --plain 2>/dev/null)
|
||||||
|
|
||||||
if [[ -z "$INFISICAL_TOKEN" ]]; then
|
if [[ -z "$INFISICAL_TOKEN" ]]; then
|
||||||
echo "❌ Infisical login fehlgeschlagen" >&2
|
echo "[start-staging] FEHLER: Infisical login fehlgeschlagen" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -24,8 +41,16 @@ export NITRO_PORT=3016
|
|||||||
export NITRO_HOST=127.0.0.1
|
export NITRO_HOST=127.0.0.1
|
||||||
export PORT=3016
|
export PORT=3016
|
||||||
|
|
||||||
|
NODE_BIN="/root/.nvm/versions/node/v24.11.1/bin/node"
|
||||||
|
INDEX_MJS="/srv/rebreak/backend/.output-staging/server/index.mjs"
|
||||||
|
|
||||||
|
if [[ ! -f "$INDEX_MJS" ]]; then
|
||||||
|
echo "[start-staging] FEHLER: $INDEX_MJS nicht gefunden — wurde deploy.sh ausgeführt?" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
exec infisical run \
|
exec infisical run \
|
||||||
--projectId="${INFISICAL_PROJECT_ID}" \
|
--projectId="${INFISICAL_PROJECT_ID:-14b11b35-ef59-4b8a-a16b-398f0cc3ad93}" \
|
||||||
--env=staging \
|
--env=staging \
|
||||||
--token="$INFISICAL_TOKEN" \
|
--token="$INFISICAL_TOKEN" \
|
||||||
-- /root/.nvm/versions/node/v24.11.1/bin/node /srv/rebreak-monorepo/backend/.output/server/index.mjs
|
-- "$NODE_BIN" "$INDEX_MJS"
|
||||||
|
|||||||
91
ecosystem.config.js
Normal file
91
ecosystem.config.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* ecosystem.config.js – PM2 Prozess-Konfiguration für Rebreak
|
||||||
|
* (backend/-Layout, post-cutover)
|
||||||
|
*
|
||||||
|
* Repo-Root: /srv/rebreak
|
||||||
|
* Backend: /srv/rebreak/backend (standalone Nitro)
|
||||||
|
* Node: /root/.nvm/versions/node/v24.11.1/bin/node
|
||||||
|
*
|
||||||
|
* Aktivierung auf Server:
|
||||||
|
* pm2 startOrReload /srv/rebreak/ecosystem.config.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NODE_BIN = "/root/.nvm/versions/node/v24.11.1/bin/node";
|
||||||
|
const REPO_ROOT = "/srv/rebreak";
|
||||||
|
const APP_DIR = `${REPO_ROOT}/backend`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
// ─── Rebreak Staging (Nitro standalone) ────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "rebreak-staging",
|
||||||
|
script: `${APP_DIR}/start-staging.sh`,
|
||||||
|
interpreter: "bash",
|
||||||
|
cwd: APP_DIR,
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: "700M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: "3016",
|
||||||
|
NITRO_PORT: "3016",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─── Rebreak Prod (Nitro standalone) ───────────────────────────────────
|
||||||
|
// Wird erst aktiviert wenn Phase 3 (DNS-Cutover) abgeschlossen ist.
|
||||||
|
// start-prod.sh wird analog start-staging.sh aufgesetzt
|
||||||
|
// (existiert noch nicht im backend/ — nicht-blockierend für Cutover).
|
||||||
|
// Start: pm2 start ecosystem.config.js --only rebreak
|
||||||
|
// {
|
||||||
|
// name: "rebreak",
|
||||||
|
// script: `${APP_DIR}/start-prod.sh`,
|
||||||
|
// interpreter: "bash",
|
||||||
|
// cwd: APP_DIR,
|
||||||
|
// instances: 1,
|
||||||
|
// autorestart: true,
|
||||||
|
// watch: false,
|
||||||
|
// max_memory_restart: "700M",
|
||||||
|
// env: {
|
||||||
|
// NODE_ENV: "production",
|
||||||
|
// PORT: "3015",
|
||||||
|
// NITRO_PORT: "3015",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// ─── Webhook-Listener ──────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "rebreak-webhook",
|
||||||
|
script: `${REPO_ROOT}/scripts/deploy-webhook/server.mjs`,
|
||||||
|
interpreter: NODE_BIN,
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: "128M",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─── DNS-Blocker (auskommentiert bis DNS-Daemons aufgesetzt sind) ──────
|
||||||
|
// {
|
||||||
|
// name: "dns-rebreak",
|
||||||
|
// script: `${APP_DIR}/server/dns/start-prod.sh`,
|
||||||
|
// interpreter: "bash",
|
||||||
|
// cwd: `${APP_DIR}/server/dns`,
|
||||||
|
// instances: 1,
|
||||||
|
// autorestart: true,
|
||||||
|
// watch: false,
|
||||||
|
// max_memory_restart: "512M",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "dns-rebreak-staging",
|
||||||
|
// script: `${APP_DIR}/server/dns/start-staging.sh`,
|
||||||
|
// interpreter: "bash",
|
||||||
|
// cwd: `${APP_DIR}/server/dns`,
|
||||||
|
// instances: 1,
|
||||||
|
// autorestart: true,
|
||||||
|
// watch: false,
|
||||||
|
// max_memory_restart: "512M",
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
};
|
||||||
407
ops/CUTOVER_PLAN.md
Normal file
407
ops/CUTOVER_PLAN.md
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
# Rebreak Cutover-Plan: `apps/rebreak/` (Nuxt) → `backend/` (Standalone Nitro)
|
||||||
|
|
||||||
|
**Verantwortlich:** Backyard-Scope (Hetzner-Pipeline + Cutover-Architektur)
|
||||||
|
**Erstellt:** 2026-05-06
|
||||||
|
**Status:** PLAN — nicht ausgeführt. Alle destruktiven Schritte sind ⚠️-markiert und bedürfen explizitem User-Approval.
|
||||||
|
**Server:** `ssh rebreak-server` (49.13.55.22, rebreak-prod-01, CX23, 4 GB RAM)
|
||||||
|
**Repo:** `git@github.com:RaynisDev/rebreak.git`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Status quo (Stand 2026-05-06, post-rollback)
|
||||||
|
|
||||||
|
### 1.1 Server `/srv/rebreak/` (live, Backyard's Layout)
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/rebreak/
|
||||||
|
├── apps/
|
||||||
|
│ ├── rebreak/ ← Nuxt-App (LIVE)
|
||||||
|
│ │ ├── nuxt.config.ts ← runtimeConfig (vollständig)
|
||||||
|
│ │ ├── start-staging.sh ← Infisical login + jq-eval + NUXT_*-Mapping
|
||||||
|
│ │ ├── server/api/... ← API-Routes
|
||||||
|
│ │ ├── .output-staging/ ← gebauter Output (pm2 fährt das)
|
||||||
|
│ │ ├── imap-proxy/ ← Mo's Scope
|
||||||
|
│ │ └── imap-idle/ ← Mo's Scope
|
||||||
|
│ └── rebreak-native/ ← React-Native-App
|
||||||
|
├── scripts/
|
||||||
|
│ ├── deploy.sh ← cd apps/rebreak + pnpm --filter @trucko/rebreak build
|
||||||
|
│ └── deploy-webhook/server.mjs ← GitHub HMAC validator → spawnt deploy.sh
|
||||||
|
├── ecosystem.config.js ← pm2 zeigt auf apps/rebreak/start-staging.sh
|
||||||
|
└── ops/nginx/ ← Source-tracking only (nicht live)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Git-HEAD:** `6eca0b5` (rolled back, Backyard's working state)
|
||||||
|
- **Branch:** `main`
|
||||||
|
- **Remote-main:** `6eca0b5` (force-pushed back to safe state)
|
||||||
|
- **pm2:** `rebreak-staging` online, `rebreak-imap-staging` online, `rebreak-idle-staging` online, `rebreak-webhook` online
|
||||||
|
- **Smoke-Tests:** `https://staging.rebreak.org/` → 200, `/api/auth/me` → 401 (correct).
|
||||||
|
|
||||||
|
### 1.2 Mac `~/mono/rebreak-monorepo/` (Cutover-Target)
|
||||||
|
|
||||||
|
```
|
||||||
|
~/mono/rebreak-monorepo/
|
||||||
|
├── backend/ ← Standalone Nitro (NEU)
|
||||||
|
│ ├── nitro.config.ts ← runtimeConfig UNVOLLSTÄNDIG (siehe §3)
|
||||||
|
│ ├── start-staging.sh ← infisical run wrapper, zeigt auf /srv/rebreak-monorepo/backend/.output/server/index.mjs
|
||||||
|
│ ├── package.json ← name: rebreak-backend
|
||||||
|
│ ├── prisma/ ← Prisma 7
|
||||||
|
│ ├── server/api/...
|
||||||
|
│ └── tsconfig.json
|
||||||
|
├── apps/rebreak-native/
|
||||||
|
├── ops/nginx/ ← gleiche Source-Tracking-Configs wie auf Server
|
||||||
|
├── pnpm-workspace.yaml ← packages: apps/* + backend
|
||||||
|
├── package.json ← name: rebreak-monorepo
|
||||||
|
└── xgit
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Git-HEAD:** `5dfbe88` (mit `cutover-llm-toggle-prep`-Arbeit, llmProvider-Toggle-Code)
|
||||||
|
- **Branch:** `main` (Mac), preserved as `cutover-llm-toggle-prep` auf Remote
|
||||||
|
- **Kein `.npmrc`** — siehe §3 Risiko Prisma 7 transitive deps
|
||||||
|
|
||||||
|
### 1.3 Was kaputt war (Incident 2026-05-06)
|
||||||
|
|
||||||
|
User force-pushte aus dem neuen Mac-Repo (`backend/`-Layout) zu `RaynisDev/rebreak.git` →
|
||||||
|
Server-Webhook triggerte `deploy.sh` → `cd /srv/rebreak/apps/rebreak` schlug fehl
|
||||||
|
(Pfad existiert nicht im neuen Layout) bzw. anderer build-pfad lief → schließlich
|
||||||
|
crashte Auth-Middleware mit HTTP 500: `Cannot read properties of undefined (reading 'url')`,
|
||||||
|
weil `backend/nitro.config.ts.runtimeConfig` keine `supabase`-Section hat und die
|
||||||
|
`@nuxtjs/supabase`-Module-Config (die das im alten Setup auto-injected) im Standalone-Nitro
|
||||||
|
nicht mehr existiert. Cascade: alle authentifizierten Endpoints kaputt.
|
||||||
|
|
||||||
|
**Rollback durchgeführt vor dieser Session:** `RaynisDev/rebreak.git` main = `6eca0b5`,
|
||||||
|
Server-HEAD = `6eca0b5`, pm2 läuft wieder, HTTP 200 + 401.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Pipeline-Diagramm
|
||||||
|
|
||||||
|
### 2.1 Pipeline AKTUELL (Backyard-Layout, läuft)
|
||||||
|
|
||||||
|
```
|
||||||
|
Mac (trucko-monorepo) GitHub Hetzner-Server (49.13.55.22)
|
||||||
|
──────────────────── ─────── ─────────────────────────────
|
||||||
|
./xgit "msg" ─push──▶ RaynisDev/rebreak.git ─hook─▶ rebreak-webhook (pm2, :9000)
|
||||||
|
│ HMAC-validate
|
||||||
|
▼
|
||||||
|
scripts/deploy.sh
|
||||||
|
│ cd /srv/rebreak
|
||||||
|
│ git fetch && reset --hard origin/main
|
||||||
|
│ pnpm install --frozen-lockfile
|
||||||
|
│ cd apps/rebreak
|
||||||
|
│ pnpm --filter @trucko/rebreak build
|
||||||
|
│ cp -r .output .output-staging-new
|
||||||
|
│ rm -rf .output-staging
|
||||||
|
│ mv .output-staging-new .output-staging
|
||||||
|
▼
|
||||||
|
pm2 restart rebreak-staging
|
||||||
|
│ → bash apps/rebreak/start-staging.sh
|
||||||
|
│ → infisical login (universal-auth)
|
||||||
|
│ → infisical secrets --env=staging --output=json
|
||||||
|
│ → eval/export VAR=val + NUXT_*-Mapping
|
||||||
|
│ → pm2 start imap-staging + idle-staging
|
||||||
|
│ → exec node apps/rebreak/.output-staging/server/index.mjs
|
||||||
|
▼
|
||||||
|
nginx: staging.rebreak.org → 127.0.0.1:3016
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Pipeline ZIEL (nach Cutover, neues Layout)
|
||||||
|
|
||||||
|
```
|
||||||
|
Mac (rebreak-monorepo) GitHub Hetzner-Server
|
||||||
|
──────────────────── ─────── ──────────────
|
||||||
|
./xgit "msg" ─push──▶ RaynisDev/rebreak.git ─hook─▶ rebreak-webhook
|
||||||
|
▼
|
||||||
|
scripts/deploy.sh ← MUSS GEÄNDERT WERDEN
|
||||||
|
│ cd /srv/rebreak
|
||||||
|
│ git fetch && reset --hard origin/main
|
||||||
|
│ pnpm install --frozen-lockfile
|
||||||
|
│ cd backend ← NEU
|
||||||
|
│ pnpm --filter rebreak-backend build ← NEU
|
||||||
|
│ cp -r .output .output-staging-new ← NEU (relativ zu backend/)
|
||||||
|
│ rm -rf .output-staging
|
||||||
|
│ mv .output-staging-new .output-staging
|
||||||
|
▼
|
||||||
|
pm2 restart rebreak-staging
|
||||||
|
│ → bash backend/start-staging.sh ← NEU
|
||||||
|
│ → infisical login + run --env=staging
|
||||||
|
│ → exec node backend/.output-staging/server/index.mjs
|
||||||
|
▼
|
||||||
|
nginx: staging.rebreak.org → 127.0.0.1:3016
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtigste Pfad-Änderungen:**
|
||||||
|
|
||||||
|
| Bereich | Vorher (live) | Nachher (Cutover) |
|
||||||
|
|---|---|---|
|
||||||
|
| App-Dir | `/srv/rebreak/apps/rebreak/` | `/srv/rebreak/backend/` |
|
||||||
|
| Build-Filter | `pnpm --filter @trucko/rebreak build` | `pnpm --filter rebreak-backend build` |
|
||||||
|
| Build-Output | `apps/rebreak/.output-staging/server/index.mjs` | `backend/.output-staging/server/index.mjs` |
|
||||||
|
| Start-Script | `apps/rebreak/start-staging.sh` | `backend/start-staging.sh` |
|
||||||
|
| ecosystem.config.js | zeigt auf apps/rebreak | zeigt auf backend |
|
||||||
|
| start-staging.sh-Pattern | jq+eval+NUXT_*-Mapping | `infisical run -- node …/.output/server/index.mjs` (kein Mapping nötig — Nitro liest `process.env.X` direkt via runtimeConfig-Defaults) |
|
||||||
|
|
||||||
|
**Achtung:** `backend/start-staging.sh` aktuell zeigt auf `/srv/rebreak-monorepo/backend/...` — falscher Pfad,
|
||||||
|
muss `/srv/rebreak/backend/...` sein (siehe §4 Schritt 6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fehlende runtimeConfig-Keys
|
||||||
|
|
||||||
|
Quelle 1: `/srv/rebreak/apps/rebreak/nuxt.config.ts` (live, vollständig)
|
||||||
|
Quelle 2: grep `config.X` in `~/mono/rebreak-monorepo/backend/server/`
|
||||||
|
Ziel: `backend/nitro.config.ts.runtimeConfig`
|
||||||
|
|
||||||
|
### 3.1 Keys die im AKTUELLEN `backend/nitro.config.ts` schon existieren (16 total)
|
||||||
|
|
||||||
|
✅ databaseUrl, adminSecret, openrouterApiKey, deepgramApiKey, googleApiKey, googleAiApiKey,
|
||||||
|
azureTtsKey, azureTtsRegion, openaiApiKey, stripeSecretKey, stripeWebhookSecret, resendApiKey,
|
||||||
|
encryptionKey, lyraBotUserId, rebreakBotUserId, groqApiKey, cronSecret +
|
||||||
|
public.{stripePublishableKey, appUrl, apiBase}
|
||||||
|
|
||||||
|
### 3.2 Keys die FEHLEN (Cutover-Blocker)
|
||||||
|
|
||||||
|
| Key | Quelle (Mac backend code) | Wofür | Default |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cartesiaApiKey` | `server/api/coach/speak-cartesia.post.ts:22` | Cartesia TTS API | `process.env.CARTESIA_API_KEY ?? ""` |
|
||||||
|
| `cartesiaVoiceId` | `server/api/coach/speak-cartesia.post.ts:24` | Cartesia voice override | `process.env.CARTESIA_VOICE_ID ?? ""` |
|
||||||
|
| `elevenlabsApiKey` | `server/api/coach/speak-elevenlabs.post.ts:26,34` | ElevenLabs TTS API | `process.env.ELEVENLABS_API_KEY ?? ""` |
|
||||||
|
| `elevenlabsVoiceId` | `server/api/coach/speak-elevenlabs.post.ts:28` | ElevenLabs voice ID | `process.env.ELEVENLABS_VOICE_ID ?? ""` |
|
||||||
|
|
||||||
|
> Diese 4 Keys sind in der live `nuxt.config.ts` **ebenfalls nicht deklariert** —
|
||||||
|
> die TTS-Routes für Cartesia/ElevenLabs sind **NEU** in der `backend/`-Welt
|
||||||
|
> (existieren als untracked files: `backend/server/api/coach/speak-cartesia.post.ts`
|
||||||
|
> + `speak-elevenlabs.post.ts`). Cutover-Risiko: gering (alte Routes existierten gar nicht),
|
||||||
|
> aber für Vollständigkeit muss `nitro.config.ts` diese Keys deklarieren, sonst
|
||||||
|
> `config.cartesiaApiKey === undefined` zur Laufzeit.
|
||||||
|
|
||||||
|
### 3.3 Supabase-Keys (Hauptbruchstelle des Incidents)
|
||||||
|
|
||||||
|
⚠️ **Im aktuellen `backend/nitro.config.ts` fehlen `supabase.url` / `supabase.key` / `supabase.serviceKey` komplett.**
|
||||||
|
|
||||||
|
Im alten Nuxt-Layout setzt `@nuxtjs/supabase` automatisch
|
||||||
|
`config.public.supabase = { url, key }` aus dem `supabase: { ... }`-Block.
|
||||||
|
Im standalone-Nitro gibt es kein Modul → muss explizit deklariert werden.
|
||||||
|
|
||||||
|
Backend-Code-Aufrufer (siehe `server/utils/auth.ts:30` etc.) lesen Supabase
|
||||||
|
typischerweise via `@supabase/supabase-js`-Client, der direkt `process.env.SUPABASE_URL` /
|
||||||
|
`SUPABASE_SERVICE_ROLE_KEY` liest. Trotzdem: für Konsistenz mit Frontend/native
|
||||||
|
und falls irgendein File `config.public.supabase.url` liest, **müssen wir
|
||||||
|
`public.supabase.{url, key}` und top-level `supabaseServiceKey` ergänzen**.
|
||||||
|
|
||||||
|
→ siehe `backend/nitro.config.ts.cutover-draft` für vollständige Liste.
|
||||||
|
|
||||||
|
### 3.4 Verifikation des Cutover-Drafts
|
||||||
|
|
||||||
|
Nach Cutover:
|
||||||
|
```
|
||||||
|
ssh rebreak-server "tr '\\0' '\\n' < /proc/$(ss -tlnp | grep :3016 | grep -oE 'pid=[0-9]+' | cut -d= -f2)/environ | grep -E 'API_KEY|SUPABASE|JWT|SECRET' | sort"
|
||||||
|
```
|
||||||
|
Liste muss alle erwarteten Werte enthalten. Wenn `process.env.X` leer ist, ist
|
||||||
|
Infisical-Wrapper kaputt — siehe `feedback_infisical_secrets.md` Verify-Trick.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cutover-Sequence (sequentiell abarbeiten, jeden Step bestätigen lassen)
|
||||||
|
|
||||||
|
> Annahme: Mac-Repo ist auf einem Pre-Cutover-Branch (z.B. `cutover-llm-toggle-prep`),
|
||||||
|
> `main` lokal = `5dfbe88`. Server `main` = `6eca0b5`. Webhook ist online.
|
||||||
|
|
||||||
|
### Phase A — Vorbereitung (nicht-destruktiv, lokal auf Mac)
|
||||||
|
|
||||||
|
**Step 1.** Aktiviere `backend/nitro.config.ts.cutover-draft` →
|
||||||
|
nach Review der Datei: `mv backend/nitro.config.ts.cutover-draft backend/nitro.config.ts`.
|
||||||
|
|
||||||
|
**Step 2.** Aktiviere `.npmrc.cutover-draft` →
|
||||||
|
`mv .npmrc.cutover-draft .npmrc` (Repo-root-level, sorgt für `node-linker=hoisted`).
|
||||||
|
|
||||||
|
**Step 3.** Korrigiere Pfad in `backend/start-staging.sh` Zeile 31:
|
||||||
|
- VORHER: `/srv/rebreak-monorepo/backend/.output/server/index.mjs`
|
||||||
|
- NACHHER: `/srv/rebreak/backend/.output-staging/server/index.mjs` (analog zur prod-pattern: gebauten output nach `.output-staging` movt deploy.sh).
|
||||||
|
|
||||||
|
**Step 4.** Lokaler Build-Test: `cd backend && pnpm install && pnpm build`. Muss `.output/server/index.mjs` produzieren.
|
||||||
|
|
||||||
|
**Step 5.** Smoke-Test lokal: `node backend/.output/server/index.mjs` mit allen ENV-Vars manuell gesetzt → Health-Check (Port 3016).
|
||||||
|
|
||||||
|
### Phase B — Server-Vorbereitung (read-only, kein Eingriff in laufendes System)
|
||||||
|
|
||||||
|
**Step 6.** SSH-Vergleich: `ssh rebreak-server "ls /srv/rebreak/"`. Verifiziere dass `backend/`-Pfad NICHT existiert (sonst stale Files vom letzten Force-Push-Versuch).
|
||||||
|
|
||||||
|
**Step 7.** ⚠️ **Falls stale `backend/` existiert:** `ssh rebreak-server "ls -la /srv/rebreak/backend/ 2>&1"` Output an User eskalieren. **NICHT selbst löschen.**
|
||||||
|
|
||||||
|
**Step 8.** Infisical-Secrets Audit: User soll bestätigen dass alle in §3.2 + §3.3 genannten
|
||||||
|
Keys (CARTESIA_API_KEY, ELEVENLABS_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY etc.)
|
||||||
|
im Infisical-staging-Env existieren. **NICHT selbst Infisical-CLI ausführen** (Token-Scope-Risiko).
|
||||||
|
|
||||||
|
### Phase C — Pipeline-Anpassung (auf Mac, dann xgit)
|
||||||
|
|
||||||
|
**Step 9.** Edit `scripts/deploy.sh` (Mac-Repo erstellen, Server hat noch alte Version):
|
||||||
|
- `APP_DIR="${REPO_ROOT}/apps/rebreak"` → `APP_DIR="${REPO_ROOT}/backend"`
|
||||||
|
- `pnpm --filter @trucko/rebreak build` → `pnpm --filter rebreak-backend build`
|
||||||
|
|
||||||
|
**Step 10.** Edit `ecosystem.config.js` (falls im neuen Repo gemirrort wird — derzeit ist es nur auf Server):
|
||||||
|
- `script: \`${APP_DIR}/start-staging.sh\`` (APP_DIR jetzt = backend)
|
||||||
|
- `cwd: APP_DIR` analog
|
||||||
|
- Webhook-Eintrag `script: \`${REPO_ROOT}/scripts/deploy-webhook/server.mjs\`` bleibt (existiert weiter unter scripts/).
|
||||||
|
|
||||||
|
> **Klärung an User:** Liegt `scripts/deploy.sh` und `ecosystem.config.js` im neuen Mac-Repo? Falls nein, müssen sie aus `/srv/rebreak/` rüberkopiert + angepasst werden. Wenn der Webhook auf dem Server diese Files NICHT aus dem Repo zieht (sondern lokal vorhält), dann werden Step 9+10 zu manuellen Server-Edits — siehe Step 14 ⚠️.
|
||||||
|
|
||||||
|
### Phase D — Deploy (DESTRUKTIV)
|
||||||
|
|
||||||
|
**Step 11. ⚠️** Sichere Backyard-State auf Server: `ssh rebreak-server "cd /srv/rebreak && git tag -a backyard-pre-cutover -m 'Pre-cutover safe-state 6eca0b5' 6eca0b5"` — User-OK einholen (read-only-tag, aber zur Sicherheit).
|
||||||
|
|
||||||
|
**Step 12. ⚠️** Force-push neuen Layout-State: aus `~/mono/rebreak-monorepo` mit `./xgit "cutover: backend/-Layout aktiviert"`.
|
||||||
|
- Webhook-Triggert deploy.sh AUTOMATISCH.
|
||||||
|
- **Risiko:** alte deploy.sh ist auf `apps/rebreak/`-Pfad fixiert, wird also `cd: apps/rebreak: No such file` werfen → build crash → kein restart, alter `.output-staging` läuft weiter.
|
||||||
|
- **Mitigation:** Der CRASH ist gewollt — er verhindert dass kaputter Code gestartet wird. Server bleibt auf laufenden `rebreak-staging` (alter index.mjs) live.
|
||||||
|
|
||||||
|
**Step 13. ⚠️** Manueller Server-Edit für `scripts/deploy.sh` UND `ecosystem.config.js`:
|
||||||
|
- **NICHT von dieser Agent-Session ausgeführt — User macht das selbst.**
|
||||||
|
- Alternativen je nach User-Wahl: (a) ssh + sed-Edit der Server-Files (b) deploy.sh wird Teil des Repos und der webhook-deploy-flow zieht es aus dem Repo (cleaner, aber selbst-referentielles Bootstrap-Problem: alter deploy.sh muss erst die neue deploy.sh ans richtige `scripts/`-Pfad kopieren).
|
||||||
|
- **Empfehlung:** (a) für ersten Cutover, danach (b) als follow-up commit.
|
||||||
|
|
||||||
|
**Step 14. ⚠️** Trigger Re-Deploy nach Server-deploy.sh-Edit: `ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh"` ODER neuer xgit-trivial-commit (z.B. README touch). User-Wahl.
|
||||||
|
|
||||||
|
**Step 15. ⚠️** Manueller pm2-restart falls deploy.sh letzten restart noch nicht getriggert hat:
|
||||||
|
`ssh rebreak-server "pm2 restart rebreak-staging --update-env"`. Das `--update-env` ist wichtig damit
|
||||||
|
neue Infisical-secrets gezogen werden.
|
||||||
|
|
||||||
|
### Phase E — Verifikation (read-only)
|
||||||
|
|
||||||
|
**Step 16.** `ssh rebreak-server "pm2 logs rebreak-staging --nostream --lines 50"` — keine 500er, kein crash-loop.
|
||||||
|
|
||||||
|
**Step 17.** Endpoint-Tests (siehe §6 Test-Checklist).
|
||||||
|
|
||||||
|
**Step 18.** Wenn 30 Minuten stable: cleanup `ssh rebreak-server "rm -rf /srv/rebreak/apps/rebreak/.output-staging /srv/rebreak/apps/rebreak/.output"`. **An User eskalieren — `rm -rf` ist destruktiv.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rollback-Plan (Cutover scheitert, <5 min)
|
||||||
|
|
||||||
|
### 5.1 Schneller Rollback (Webhook re-triggert alter HEAD)
|
||||||
|
|
||||||
|
```
|
||||||
|
# Aus Mac-Repo:
|
||||||
|
git checkout main
|
||||||
|
git reset --hard 6eca0b5 # Backyard's safe state
|
||||||
|
git push --force origin main # ⚠️ destructiv, User-OK
|
||||||
|
|
||||||
|
# Dann auf Server:
|
||||||
|
ssh rebreak-server "cd /srv/rebreak && git reset --hard 6eca0b5" # ⚠️
|
||||||
|
ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh" # alter deploy.sh, alter Pfad
|
||||||
|
ssh rebreak-server "pm2 restart rebreak-staging --update-env"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Falls Server `scripts/deploy.sh` schon im Cutover modifiziert wurde
|
||||||
|
|
||||||
|
User muss manuell die alte Version aus `git show 6eca0b5:scripts/deploy.sh > /srv/rebreak/scripts/deploy.sh` schreiben (read-only-Mac, dann scp), bevor Re-Deploy klappt.
|
||||||
|
|
||||||
|
### 5.3 Falls `.output-staging` bereits gelöscht wurde
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh rebreak-server "cd /srv/rebreak/apps/rebreak && pnpm --filter @trucko/rebreak build && cp -r .output .output-staging"
|
||||||
|
```
|
||||||
|
Vorher User-OK einholen (Build dauert 90-180s, zwischenzeitlich keine Auth möglich).
|
||||||
|
|
||||||
|
### 5.4 Smoke-Test nach Rollback
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/api/auth/me
|
||||||
|
# Erwartung: 200 + 401
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Test-Checklist nach Cutover
|
||||||
|
|
||||||
|
**Pflicht (alle müssen pass):**
|
||||||
|
|
||||||
|
- [ ] `GET https://staging.rebreak.org/` → 200
|
||||||
|
- [ ] `GET https://staging.rebreak.org/api/auth/me` → 401 (unauthenticated, NICHT 500)
|
||||||
|
- [ ] `GET https://staging.rebreak.org/api/auth/me` mit gültigem JWT → 200 + user-data
|
||||||
|
- [ ] `POST https://staging.rebreak.org/api/coach/sos-session` mit JWT → 200 + sessionId
|
||||||
|
- [ ] `GET https://staging.rebreak.org/api/coach/sos-stream?session=<id>` → SSE stream openend
|
||||||
|
- [ ] `POST /api/coach/speak-google` mit JWT → audio/mpeg blob
|
||||||
|
- [ ] `POST /api/coach/speak-deepgram` mit JWT → audio blob
|
||||||
|
- [ ] `POST /api/coach/speak-gemini` mit JWT → audio blob
|
||||||
|
- [ ] `POST /api/coach/speak-cartesia` mit JWT → audio blob (NEU)
|
||||||
|
- [ ] `POST /api/coach/speak-elevenlabs` mit JWT → audio blob (NEU)
|
||||||
|
- [ ] `POST /api/coach/transcribe` mit audio + JWT → text
|
||||||
|
- [ ] `GET /api/community/posts` mit JWT → posts-array
|
||||||
|
- [ ] `GET /api/dns/profile` mit JWT → profile-config (achtet auf `config.public.appUrl.includes("staging")`)
|
||||||
|
- [ ] `GET /api/lyra/welcome-back` mit JWT → message
|
||||||
|
- [ ] `POST /api/stripe/checkout` mit JWT → session-url
|
||||||
|
|
||||||
|
**Best-effort (admin/cron):**
|
||||||
|
|
||||||
|
- [ ] `GET /api/admin/stats?secret=<adminSecret>` → 200
|
||||||
|
- [ ] `POST /api/cron/lyra-post?secret=<cronSecret>` → 200/204
|
||||||
|
|
||||||
|
**Process-Health:**
|
||||||
|
|
||||||
|
- [ ] `pm2 logs rebreak-staging --nostream --lines 100` — keine 500er, keine crash-restarts
|
||||||
|
- [ ] `pm2 jlist | jq '.[] | select(.name=="rebreak-staging") | .pm2_env.restart_time'` → 0 increments über 5 min
|
||||||
|
- [ ] `tr '\0' '\n' < /proc/<node-pid>/environ | grep -E 'API_KEY|SUPABASE'` → alle keys present
|
||||||
|
|
||||||
|
**iOS/Android-App-Test (User-Side):**
|
||||||
|
|
||||||
|
- [ ] App-Login → User-Profile lädt
|
||||||
|
- [ ] SOS-Session öffnen → Lyra streamt + spricht (TtsProvider beide testen)
|
||||||
|
- [ ] Community-Tab → posts laden
|
||||||
|
- [ ] DNS-Profile-Sync läuft
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risiken + Open Questions
|
||||||
|
|
||||||
|
### Risiken
|
||||||
|
|
||||||
|
- **R1 (high):** `scripts/deploy.sh` und `ecosystem.config.js` sind auf dem Server, NICHT im Repo. Cutover-Bootstrap ist selbstreferentiell — entweder manueller Server-Edit (Step 13) ODER Repo-Migration der Files. Diese Sequenz hat ein narrow window in dem ein altes deploy.sh gegen neue Repo-Struktur baut → fail.
|
||||||
|
- **R2 (medium):** `node-linker=hoisted` in `.npmrc` muss BEFORE first `pnpm install` aktiv sein, sonst landet `@prisma/client-runtime-utils` in einem nested `node_modules/.pnpm/...` und ESM-Loader findet's nicht. Step 2 muss vor Step 4 passieren. Strikte Order.
|
||||||
|
- **R3 (medium):** `start-staging.sh` im neuen Backend nutzt `infisical run -- node ...` — beim alten gab es `eval` von secrets-JSON. Falls irgendeine ENV-Var-Mapping-Magie (NUXT_*-Prefix) gebraucht wird, muss sie nachgezogen werden. Audit nötig: existiert ein Code-Pfad der `process.env.NUXT_X` direkt liest und nicht via `useRuntimeConfig`?
|
||||||
|
- **R4 (low):** PWA-Manifest + i18n-locales aus `nuxt.config.ts` haben kein Äquivalent in standalone Nitro. Falls App-Frontend (rebreak-native) auf SSR-i18n-Endpoints angewiesen ist → kaputt. Native-App-Test im Detail.
|
||||||
|
- **R5 (low):** `nitro.config.externals.inline: [/^(?!@supabase\/supabase-js)/]` ist eine Anti-pattern-Regex (matched fast alles). Möglicherweise produziert sie unerwartet riesige `.output`-bundles. Code-Review im Cutover-PR sinnvoll.
|
||||||
|
|
||||||
|
### Open Questions an User
|
||||||
|
|
||||||
|
1. **Sollen `scripts/deploy.sh` + `ecosystem.config.js` Teil des Cutover-Commits werden** (im Repo unter `scripts/` und `ecosystem.config.js` Root)? Das vermeidet manuelle Server-Edits und ist Backyard-konform. Bootstrap-Frage: einmaliger manueller `git pull && cp scripts/deploy.sh /srv/rebreak/scripts/` first-time, danach automatisch.
|
||||||
|
2. **`backend/start-staging.sh` Pfad:** soll auf `/srv/rebreak/backend/.output-staging/server/index.mjs` zeigen (analog Backyard's apps/rebreak-Pattern, mit deploy.sh-`.output → .output-staging` move) ODER direkt `/srv/rebreak/backend/.output/server/index.mjs` (ohne staging-suffix-Move)? Erste Variante = Konsistenz mit Backyard. Zweite = simpler Cutover.
|
||||||
|
3. **Webhook-Build-Filter:** soll der Filter `pnpm --filter rebreak-backend build` den Frontend-Native-App-Build IGNORIEREN (bisher: nur Backend baut)? Die rebreak-native-App buildet via Capacitor/Metro, nicht via pnpm — also ist `--filter rebreak-backend` korrekt restriktiv. Bestätigen.
|
||||||
|
4. **Gibt es eine Test-Staging-Umgebung** wo wir den Cutover proben können bevor wir die echte staging.rebreak.org nehmen? Andernfalls ist DiGA-User-Test-Window (TestFlight) während Cutover potentiell down.
|
||||||
|
5. **Cartesia/ElevenLabs-Secrets in Infisical:** existieren `CARTESIA_API_KEY` und `ELEVENLABS_API_KEY` schon im staging-Env? Falls nein, sind die neuen TTS-Routes nach Cutover broken (404-degradation, nicht 500 — `if (!key) return error`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Operations-Cheatsheet (post-Cutover)
|
||||||
|
|
||||||
|
```
|
||||||
|
# Logs live
|
||||||
|
ssh rebreak-server "pm2 logs rebreak-staging --lines 100"
|
||||||
|
|
||||||
|
# Manueller Re-Deploy (nur bei broken webhook)
|
||||||
|
ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh"
|
||||||
|
|
||||||
|
# pm2-status
|
||||||
|
ssh rebreak-server "pm2 list"
|
||||||
|
|
||||||
|
# Verify .output-staging is fresh
|
||||||
|
ssh rebreak-server "stat -c '%y' /srv/rebreak/backend/.output-staging/server/index.mjs"
|
||||||
|
|
||||||
|
# Verify config-key present in built bundle
|
||||||
|
ssh rebreak-server "grep -l 'cartesiaApiKey' /srv/rebreak/backend/.output-staging/server/chunks/*.mjs"
|
||||||
|
|
||||||
|
# Check ENV-Vars in running node-process
|
||||||
|
ssh rebreak-server "tr '\\0' '\\n' < /proc/\$(ss -tlnp | grep :3016 | grep -oE 'pid=[0-9]+' | cut -d= -f2)/environ | grep -E 'CARTESIA|ELEVENLABS|SUPABASE' | sort"
|
||||||
|
|
||||||
|
# nginx reload (NUR wenn nginx-conf changed; wir machen das nicht in diesem Cutover)
|
||||||
|
ssh rebreak-server "nginx -t && systemctl reload nginx"
|
||||||
|
```
|
||||||
|
|
||||||
|
**KEIN-GO-Liste (gilt während Cutover und danach):**
|
||||||
|
|
||||||
|
- ❌ `pm2 delete rebreak-staging` (nur restart)
|
||||||
|
- ❌ `git reset --hard` direkt auf Server (deploy.sh macht das offiziell)
|
||||||
|
- ❌ `rm -rf` auf `/srv/rebreak/...` ohne explizites User-OK
|
||||||
|
- ❌ DNS-Änderungen
|
||||||
|
- ❌ Infisical-Token-Rotation
|
||||||
|
- ❌ `pnpm install` parallel zum Webhook-Deploy (OOM auf 4 GB)
|
||||||
175
scripts/deploy-webhook/server.mjs
Normal file
175
scripts/deploy-webhook/server.mjs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Rebreak GitHub Webhook Listener
|
||||||
|
*
|
||||||
|
* Empfängt GitHub push-Events, validiert HMAC-SHA256-Signatur,
|
||||||
|
* und triggert das deploy.sh Script im Hintergrund.
|
||||||
|
*
|
||||||
|
* Port: 9000 (intern, wird von nginx reverse-proxied)
|
||||||
|
* Secret: GITHUB_WEBHOOK_SECRET in /etc/environment (via Infisical)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from "http";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
const REPO_ROOT = "/srv/rebreak";
|
||||||
|
const DEPLOY_SCRIPT = `${REPO_ROOT}/scripts/deploy.sh`;
|
||||||
|
const PORT = 9000;
|
||||||
|
|
||||||
|
// Lade GITHUB_WEBHOOK_SECRET aus /etc/environment
|
||||||
|
function loadEnvFile() {
|
||||||
|
try {
|
||||||
|
const content = readFileSync("/etc/environment", "utf-8");
|
||||||
|
for (const line of content.split("\n")) {
|
||||||
|
const match = line.match(/^([^=]+)="?([^"]*)"?$/);
|
||||||
|
if (match) process.env[match[1]] = match[2];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// /etc/environment nicht lesbar – Env-Vars müssen anderweitig gesetzt sein
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadEnvFile();
|
||||||
|
|
||||||
|
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
|
||||||
|
if (!WEBHOOK_SECRET) {
|
||||||
|
console.error(
|
||||||
|
"[Webhook] FATAL: GITHUB_WEBHOOK_SECRET nicht in /etc/environment gesetzt",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifySignature(secret, signature, payload) {
|
||||||
|
const hmac = crypto.createHmac("sha256", secret);
|
||||||
|
hmac.update(payload, "utf-8");
|
||||||
|
const digest = `sha256=${hmac.digest("hex")}`;
|
||||||
|
try {
|
||||||
|
return crypto.timingSafeEqual(
|
||||||
|
Buffer.from(signature),
|
||||||
|
Buffer.from(digest),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy-Queue: verhindert parallele Builds (OOM-Schutz auf 4 GB RAM)
|
||||||
|
let deployRunning = false;
|
||||||
|
let pendingDeploy = false;
|
||||||
|
|
||||||
|
function runDeploy() {
|
||||||
|
if (deployRunning) {
|
||||||
|
pendingDeploy = true;
|
||||||
|
console.log("[Webhook] Deploy already running – queued next deploy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deployRunning = true;
|
||||||
|
console.log("[Webhook] Starting deploy.sh...");
|
||||||
|
|
||||||
|
const proc = spawn("bash", [DEPLOY_SCRIPT], {
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
detached: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stdout.on("data", (d) => process.stdout.write(`[deploy] ${d}`));
|
||||||
|
proc.stderr.on("data", (d) => process.stderr.write(`[deploy:err] ${d}`));
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
deployRunning = false;
|
||||||
|
console.log(`[Webhook] deploy.sh exited with code ${code}`);
|
||||||
|
if (pendingDeploy) {
|
||||||
|
pendingDeploy = false;
|
||||||
|
console.log("[Webhook] Running queued deploy...");
|
||||||
|
runDeploy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
deployRunning = false;
|
||||||
|
console.error("[Webhook] deploy.sh spawn error:", err.message);
|
||||||
|
if (pendingDeploy) {
|
||||||
|
pendingDeploy = false;
|
||||||
|
runDeploy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.method !== "POST" || req.url !== "/webhook") {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => (body += chunk));
|
||||||
|
req.on("end", () => {
|
||||||
|
const sig = req.headers["x-hub-signature-256"];
|
||||||
|
if (!sig) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Missing signature" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifySignature(WEBHOOK_SECRET, sig, body)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Invalid signature" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(body);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = payload.ref?.split("/").pop();
|
||||||
|
if (branch !== "main") {
|
||||||
|
console.log(`[Webhook] Ignoring non-main branch: ${branch}`);
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ ok: false, reason: "Not main branch", branch }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedFiles = (payload.commits || []).flatMap((c) => [
|
||||||
|
...(c.added || []),
|
||||||
|
...(c.modified || []),
|
||||||
|
...(c.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Webhook] Push to main by ${payload.pusher?.name} – ${changedFiles.length} files changed`,
|
||||||
|
);
|
||||||
|
console.log("[Webhook] Changed files:", changedFiles.slice(0, 10));
|
||||||
|
|
||||||
|
res.writeHead(202);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
message: deployRunning
|
||||||
|
? "Deploy queued (previous still running)"
|
||||||
|
: "Deploy started",
|
||||||
|
files: changedFiles.length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Asynchron deployen – Response ist bereits gesendet
|
||||||
|
runDeploy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, "127.0.0.1", () => {
|
||||||
|
console.log(`[Webhook] Listening on http://127.0.0.1:${PORT}/webhook`);
|
||||||
|
console.log(`[Webhook] Secret loaded: ${WEBHOOK_SECRET ? "yes" : "NO – FATAL"}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
console.log("[Webhook] SIGTERM received, shutting down");
|
||||||
|
server.close(() => process.exit(0));
|
||||||
|
});
|
||||||
93
scripts/deploy.sh
Executable file
93
scripts/deploy.sh
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# deploy.sh – Rebreak Deploy Script (backend/-Layout, post-cutover)
|
||||||
|
#
|
||||||
|
# Wird vom Webhook-Listener (scripts/deploy-webhook/server.mjs) aufgerufen.
|
||||||
|
# Repo-Root: /srv/rebreak
|
||||||
|
# Clone: git@github.com:RaynisDev/rebreak.git
|
||||||
|
# Backend: /srv/rebreak/backend (standalone Nitro, package: rebreak-backend)
|
||||||
|
#
|
||||||
|
# Ablauf:
|
||||||
|
# 1. Git pull (via Deploy-Key)
|
||||||
|
# 2. pnpm install --frozen-lockfile (mit hoisted node-linker via .npmrc)
|
||||||
|
# 3. cd backend && pnpm --filter rebreak-backend build (Prisma generate + Nitro build)
|
||||||
|
# 4. .output → .output-staging (atomisch via tmp)
|
||||||
|
# 5. pm2 restart rebreak-staging --update-env
|
||||||
|
# 6. pm2 restart rebreak-imap-staging / rebreak-idle-staging (best-effort, falls vorhanden)
|
||||||
|
# 7. pm2 restart dns-rebreak-staging / dns-rebreak (best-effort, falls vorhanden)
|
||||||
|
#
|
||||||
|
# Secrets: via Infisical (INFISICAL_CLIENT_ID/SECRET in /etc/environment) — NICHT hier,
|
||||||
|
# sondern in start-staging.sh / ecosystem.config.js zur Laufzeit.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="/srv/rebreak"
|
||||||
|
APP_DIR="${REPO_ROOT}/backend"
|
||||||
|
NODE_BIN="/root/.nvm/versions/node/v24.11.1/bin/node"
|
||||||
|
PNPM_BIN="/root/.nvm/versions/node/v24.11.1/bin/pnpm"
|
||||||
|
PM2_BIN="/root/.nvm/versions/node/v24.11.1/bin/pm2"
|
||||||
|
|
||||||
|
log() { echo "[deploy] $(date '+%H:%M:%S') $*"; }
|
||||||
|
log_err() { echo "[deploy:err] $(date '+%H:%M:%S') $*" >&2; }
|
||||||
|
|
||||||
|
log "=== Rebreak Deploy gestartet (backend/-Layout) ==="
|
||||||
|
|
||||||
|
# 0. Sicherstellen dass PATH stimmt
|
||||||
|
export PATH="/root/.nvm/versions/node/v24.11.1/bin:$PATH"
|
||||||
|
|
||||||
|
# 1. Git pull via Deploy-Key (SSH ist konfiguriert in /root/.ssh/config)
|
||||||
|
log "Step 1: git pull..."
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
git fetch origin main 2>&1
|
||||||
|
git reset --hard origin/main 2>&1
|
||||||
|
git clean -fd 2>&1
|
||||||
|
log "Git updated to $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
# 2. pnpm install (workspace-root, .npmrc mit node-linker=hoisted ist im Repo)
|
||||||
|
log "Step 2: pnpm install --frozen-lockfile..."
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
CI=true "${PNPM_BIN}" install --frozen-lockfile 2>&1 || {
|
||||||
|
log_err "frozen-lockfile fehlgeschlagen, fallback ohne frozen..."
|
||||||
|
CI=true "${PNPM_BIN}" install --no-frozen-lockfile 2>&1
|
||||||
|
}
|
||||||
|
log "pnpm install done"
|
||||||
|
|
||||||
|
# 3. Build backend (Nitro standalone) — Prisma generate ist Teil des build-scripts
|
||||||
|
log "Step 3: pnpm --filter rebreak-backend build..."
|
||||||
|
cd "${APP_DIR}"
|
||||||
|
# NODE_OPTIONS: max 1.5 GB für den Build-Prozess (4 GB RAM Server)
|
||||||
|
NODE_OPTIONS="--max-old-space-size=1536" CI=true "${PNPM_BIN}" --filter rebreak-backend build 2>&1
|
||||||
|
log "Build done"
|
||||||
|
|
||||||
|
# 4. Atomisches Deploy: .output → .output-staging (relativ zu backend/)
|
||||||
|
log "Step 4: Atomisches Deploy .output → .output-staging..."
|
||||||
|
cd "${APP_DIR}"
|
||||||
|
if [ -d ".output" ]; then
|
||||||
|
rm -rf .output-staging-new
|
||||||
|
cp -r .output .output-staging-new
|
||||||
|
rm -rf .output-staging
|
||||||
|
mv .output-staging-new .output-staging
|
||||||
|
log ".output-staging aktualisiert"
|
||||||
|
else
|
||||||
|
log_err "FEHLER: .output Verzeichnis nicht gefunden nach Build!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. pm2 restart rebreak-staging (--update-env zieht neue Infisical-secrets)
|
||||||
|
log "Step 5: pm2 restart rebreak-staging..."
|
||||||
|
"${PM2_BIN}" restart rebreak-staging --update-env 2>/dev/null || \
|
||||||
|
"${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only rebreak-staging
|
||||||
|
log "rebreak-staging restarted"
|
||||||
|
|
||||||
|
# 6. IMAP + DNS Services (optional – kein Fehler wenn nicht vorhanden, Mo's Scope)
|
||||||
|
log "Step 6: Optional services restart..."
|
||||||
|
"${PM2_BIN}" restart rebreak-imap-staging 2>/dev/null || true
|
||||||
|
"${PM2_BIN}" restart rebreak-idle-staging 2>/dev/null || true
|
||||||
|
"${PM2_BIN}" restart dns-rebreak-staging 2>/dev/null || \
|
||||||
|
"${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only dns-rebreak-staging 2>/dev/null || true
|
||||||
|
"${PM2_BIN}" restart dns-rebreak 2>/dev/null || \
|
||||||
|
"${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only dns-rebreak 2>/dev/null || true
|
||||||
|
|
||||||
|
# 7. pm2 save
|
||||||
|
"${PM2_BIN}" save 2>/dev/null || true
|
||||||
|
|
||||||
|
log "=== Deploy erfolgreich: $(git -C ${REPO_ROOT} rev-parse --short HEAD) ==="
|
||||||
Loading…
x
Reference in New Issue
Block a user