chahinebrini d1b71e76b2 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>
2026-05-07 00:35:50 +02:00

176 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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));
});