rebreak-monorepo/ops/GITHUB_ACTIONS_PIPELINE.md
chahinebrini 87438ede8e feat(deploy): GitHub-Actions Build+Deploy-Pipeline für rebreak-staging
CX23 (4GB RAM) OOM'd am 2026-05-06 während webhook-getriggertem `pnpm build`
(Heap-Limit 1.5GB überschritten). Build raus aus Server, GitHub-Runner (7GB RAM)
übernimmt — Server deployed nur noch Artifact via scp + atomic-mv + pm2 restart.

- .github/workflows/deploy-staging.yml: 2-Job (build + deploy via SSH-Artifact-Push)
- scripts/deploy-from-artifact.sh: Server-Script mit Migration-Detection + atomic-mv
- ops/GITHUB_ACTIONS_PIPELINE.md: Architektur-Doku + Cheatsheet

Coexistence: alter rebreak-webhook bleibt als Failsafe, wird nach 5+ erfolgreichen
GA-Runs deaktiviert. Erster Run: Webhook temporär gestoppt für sauberen Test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:18:49 +02:00

12 KiB

GitHub-Actions Build+Deploy-Pipeline -- rebreak-monorepo

Owner: backyard (Infrastruktur-Architekt) Stand: 2026-05-07 Status: Files geschrieben (Workflow + Server-Script + dieses Doc) -- noch NICHT auf main gepushed, GitHub-Secrets noch NICHT angelegt.


Warum diese Pipeline jetzt

Am 2026-05-06 hat der Hetzner CX23 (4 GB RAM, 1.5 GB Heap-Limit fuer Node) waehrend des Webhook-getriggerten pnpm build OOM'd:

FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory

Der laufende rebreak-staging-Prozess hat den Crash UEBERLEBT (pm2 nicht restartet, alte .output-staging aktiv, HTTP 401 ok). Aber der naechste Push waere genauso gefailt -- und irgendwann wuerde der pm2-restart auf eine korrupt-extrahierte .output-staging greifen und in den Crash-Loop gehen (siehe Backyard-Memory feedback_deploy_workflow.md).

User-Decision (2026-05-08): GitHub Actions baut, Hetzner deployt nur noch das Artifact. Server bleibt CX23 (keine Cost-Hochskalierung weil Rebreak noch nicht monetarisiert).


Recon: Trucko-Pattern (Vorlagen)

/Users/chahinebrini/mono/trucko-monorepo/.github/workflows/ -- alle drei Workflows sind aktuell .disabled, aber der Code laeuft als Vorlage:

File Was wir uebernehmen
android.yml.disabled Hauptvorlage -- SSH-Setup-Pattern (HETZNER_SSH_KEY / HETZNER_HOST / HETZNER_USER als GitHub-Secrets), ssh-keyscan -H, scp zum Server, mkdir -p ~/.ssh, printf '%s\n' "$SSH_PRIVATE_KEY". Plus die concurrency-group + workflow_dispatch + Artifact-Upload-Idee
ci.yml.disabled Aufbau-Pattern: pnpm/action-setup + setup-node mit cache: pnpm + pnpm install --frozen-lockfile. Plus die concurrency: cancel-in-progress Idee (bei uns aber false, weil Deploys nicht abgebrochen werden duerfen)
e2e.yml.disabled Infisical-Secret-Loading-Pattern (falls wir spaeter Test-Jobs brauchen). Aktuell NICHT relevant -- Tests kommen in eigenem Workflow wenn Coverage existiert

Wir schreiben keinen ci.yml-Klon -- Phase 6 explizit: keine Tests in der Pipeline (Ahmed's TESTING_STATE.md hat Coverage 0%).


Neue Pipeline-Architektur

   GitHub-Push (main)
        |
        v
 .github/workflows/deploy-staging.yml
        |
        +--[Job 1: build]----------------------+
        |  ubuntu-latest (7 GB RAM)            |
        |  - checkout (fetch-depth=0)          |
        |  - pnpm/setup-node@v4 (24.11.1)      |
        |  - pnpm install --frozen-lockfile    |
        |  - cd backend && pnpm build          |
        |    (= prisma generate + nitro build) |
        |  - tar czf backend-output.tar.gz     |
        |  - upload-artifact@v4 (7d retention) |
        +--------------------------------------+
        |
        v
        +--[Job 2: deploy]---------------------+
        |  ubuntu-latest                       |
        |  environment: staging                |
        |  - download-artifact@v4              |
        |  - SSH-Setup mit HETZNER_SSH_KEY     |
        |  - scp tar.gz -> /srv/rebreak/       |
        |    backend/.output-incoming.tar.gz   |
        |  - ssh -> deploy-from-artifact.sh    |
        |  - health-check curl /api/auth/me    |
        |    (erwartet HTTP 401)               |
        +--------------------------------------+
        |
        v
   Hetzner CX23 (Server)
   /srv/rebreak/scripts/deploy-from-artifact.sh
        |
        +-- git pull (fuer migrations + scripts)
        +-- prisma migrate deploy (wenn schema/migrations changed)
        +-- pnpm install --frozen-lockfile (runtime-deps)
        +-- tar xzf -> .output-staging-new -> mv .output-staging
        +-- pm2 restart rebreak-staging --update-env
        +-- echo SHA > .last-deployed-sha

Server baut nicht mehr selbst. Build ist aus dem Server raus -- OOM-Risiko gelo:est.

Files in diesem PR

  • .github/workflows/deploy-staging.yml (neu)
  • scripts/deploy-from-artifact.sh (neu, +x via git nach commit)
  • ops/GITHUB_ACTIONS_PIPELINE.md (dieses Doc)

scripts/deploy.sh und scripts/deploy-webhook/server.mjs bleiben unveraendert (Coexistence -- siehe unten).


Required GitHub-Secrets

User muss in GitHub Repo Settings -> Environments -> staging -> Add Secret:

Secret-Name Beschreibung Quelle
HETZNER_SSH_KEY Privater SSH-Key (ed25519) der zu /root/.ssh/authorized_keys auf 49.13.55.22 pusht. Empfehlung: neuen Deploy-Key generieren (ssh-keygen -t ed25519 -f ~/.ssh/rebreak-deploy -C "github-actions@rebreak"), public part in /root/.ssh/authorized_keys auf Hetzner anhaengen, private part als Secret. Recycling-Option: falls der trucko-Workflow schon einen Hetzner-Deploy-Key hatte (gleiche Zielmaschine api.trucko.org bzw. heute 49.13.55.22), kann der wiederverwendet werden -- Doku-Quelle: trucko .github/workflows/android.yml.disabled Zeile 134 nutzt HETZNER_SSH_KEY ebenfalls User generiert
HETZNER_HOST staging.rebreak.org (resolved direkt auf 49.13.55.22, kein Cloudflare-Proxy). NICHT api.trucko.org -- das zeigt auf 128.140.47.53 (anderen, alten shared-Server). DNS-Layer-Indirection bevorzugt damit Server-Migration ohne Secret-Rotation moeglich ist Statisch
HETZNER_USER root Statisch

Warum Environment "staging" und nicht Repo-Secrets:

  • Environment-Secrets sind nur fuer Jobs mit environment: staging zugaenglich -- Build-Job (ohne environment-Tag) sieht sie nicht. Saubere Privilege-Separation.
  • Spaeter beim deploy-prod.yml legen wir Environment "production" mit eigener HETZNER_SSH_KEY_PROD an (oder gleiches Key-Pair, je nach User-Praeferenz).
  • Environment-Settings koennen "Required reviewers" haben -- fuer production empfohlen (manuelle Approval vor jedem Deploy), fuer staging nicht noetig.

Required User-Actions vor erstem GA-Run

In dieser Reihenfolge:

  1. Deploy-Key generieren + auf Server pushen:

    # Lokal:
    ssh-keygen -t ed25519 -f ~/.ssh/rebreak-deploy -C "github-actions@rebreak" -N ""
    cat ~/.ssh/rebreak-deploy.pub
    # Output kopieren, dann:
    ssh root@49.13.55.22 "echo '<paste>' >> /root/.ssh/authorized_keys"
    # SSH-Test:
    ssh -i ~/.ssh/rebreak-deploy root@49.13.55.22 "whoami"  # erwartet: root
    
  2. Server-Script deployen (einmalig, ueber den noch laufenden Webhook):

    • PR mit den drei Files mergen -> Webhook triggert scripts/deploy.sh -> der pullt das neue Repo inklusive scripts/deploy-from-artifact.sh. Danach manuell chmod +x via SSH:
      ssh root@49.13.55.22 "chmod +x /srv/rebreak/scripts/deploy-from-artifact.sh"
      
    • Alternative: chmod +x lokal vor dem Commit setzen (git update-index --chmod=+x scripts/deploy-from-artifact.sh) -- dann bringt git den Modus mit. Empfohlen weil reproducibel.
  3. GitHub-Environment "staging" anlegen:

    • Repo Settings -> Environments -> New environment -> Name: staging
    • Add Secret: HETZNER_SSH_KEY (Inhalt von ~/.ssh/rebreak-deploy)
    • Add Secret: HETZNER_HOST (49.13.55.22)
    • Add Secret: HETZNER_USER (root)
    • Optional: "Wait timer 0 min" + "Required reviewers: none" fuer staging.
  4. Manuell triggern fuer ersten Test-Run:

    • Repo -> Actions -> "Deploy Staging" -> Run workflow -> Branch main -> Run.
    • Erwartung: Build-Job 3-5 min, Deploy-Job 1-2 min, Health-Check passes.
  5. Smoke-Test via curl:

    curl -sS -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/api/auth/me
    # erwartet: 401
    

Coexistence-Strategie: Webhook bleibt parallel

Empfehlung: (b) parallel lassen fuer 1-2 Wochen als Failsafe, dann (a) Webhook-Auto-Deploy DEAKTIVIEREN sobald 5+ erfolgreiche GA-Deploys ohne Issues durchgelaufen sind.

Warum nicht direkt (a) abschalten:

  • Webhook-Listener auf Hetzner laeuft seit URL-Fix stabil (2026-05-06 OOM-Event hat ihn nicht gekillt -- Listener selbst hat 60 MB RAM-Footprint, OOM-Killer hat den Build-Prozess geholt nicht den Listener).
  • GitHub-Actions ist eine neue Komponente -- bevor wir das alte Sicherheitsnetz wegnehmen, wollen wir mehrere erfolgreiche Runs sehen.
  • Beide Pipelines schreiben in .output-staging mit atomic-mv -- kein Race wenn nicht gleichzeitig getriggert. Risk: falls Webhook + GA gleichzeitig laufen (Push-Event fuert beide), gibt es zwei pnpm-installs nacheinander -- nicht schlimm, aber un-elegant.
  • Mitigation: im scripts/deploy.sh einen Soft-Bail einbauen: wenn .output-incoming.tar.gz neuer als 60 s -> "GA-Deploy laeuft, Webhook skipped, exit 0". Optionaler Verbessungsschritt, NICHT in diesem PR.

Cutover-Plan (User-Decision in 1-2 Wochen):

  • Webhook-Deaktivierung: GitHub Repo Settings -> Webhooks -> Edit https://staging.rebreak.org/webhook -> "Active" Checkbox aus.
  • pm2 stop: ssh root@49.13.55.22 "pm2 stop rebreak-webhook && pm2 save".
  • scripts/deploy.sh als Reference behalten (oder nach scripts/legacy/deploy.sh archivieren).

Migration zu Production-Pipeline (spaeter)

Nicht in dieser Phase. Vorgeplant:

  • Eigener Workflow .github/workflows/deploy-prod.yml
  • Trigger: nur via workflow_dispatch (kein automatischer Push-Trigger -- prod ist nicht main, prod ist tag/release-driven)
  • Environment: production mit Required-Reviewers + 5-min-Wait-Timer
  • Server-Script: scripts/deploy-from-artifact-prod.sh -- analog, aber zielt auf .output-prod und pm2 restart rebreak (statt -staging)
  • Build-Job kann mit deploy-staging.yml geteilt werden via composite-action (./.github/actions/build-backend)

Voraussetzung: Prod-Service rebreak muss erstmal auf dem Server existieren. Aktuell laufen nur rebreak-staging + rebreak-webhook + Mo's IMAP/IDLE/DNS-Services.


Open Questions

  1. Deploy-Key recyceln vs neu? Falls ein alter trucko-Hetzner-Deploy-Key existiert (Backyard hat keine Sicht in ~/.ssh/ von User) -- recycling spart Setup-Arbeit, aber neu generieren ist sicherer (klare Bindung "github-actions-rebreak", einfacher rotierbar). Empfehlung: neu.
  2. Health-Check streng oder lax? Aktuelles Workflow-Snippet checkt HTTP 401 (auth-protected -> Server lebt). Strikter waere 200 von einem Public-Health-Endpoint -- aber ich sehe keinen /api/health im Backend. Soll ich einen /api/health GET-Endpoint vorschlagen fuer rebreak-backend-Owner? Out-of-scope hier.
  3. GA-Free-Tier-Quota: Privates Repo hat 2000 Build-Minuten/Monat im Free-Tier. Build-Job 3-5 min + Deploy-Job 1-2 min = ~7 min pro Push. 2000 / 7 = ~285 Pushes/Monat. Reichen wahrscheinlich, aber bei viel Refactor-Phase koennte das Quota knapp werden -- dann selfhosted-runner-Option auf dem Hetzner selbst (aber das bringt das OOM-Problem zurueck). Decision-Trigger: wenn wir > 200 Deploys/Monat sehen, neu evaluieren.
  4. pnpm install auf Server doppelt? Aktuell installiert deploy-from-artifact.sh Step 3 nochmal pnpm install --frozen-lockfile. Das ist fuer Runtime-Module (z.B. @prisma/client mit native .node-Files, pg mit native bindings) noetig, weil das Artifact nur Nitro-Server-Bundle enthaelt, nicht node_modules/. Optimierung: Artifact koennte komplett node_modules/ mitbringen (tar groesser, ~100-200 MB), dann faellt Server-side-Install weg. Trade-off: Upload-Time vs Install-Time. Fuer jetzt: zwei-Schritt-Layout ist robust. Spaeter optimierbar.

Cheatsheet

# Manuell triggern
gh workflow run deploy-staging.yml

# Letzten Run anschauen
gh run list --workflow=deploy-staging.yml --limit 5
gh run view <run-id> --log

# Webhook (Legacy) deaktivieren
ssh root@49.13.55.22 "pm2 stop rebreak-webhook && pm2 save"
# GitHub Repo -> Settings -> Webhooks -> Active off

# Last-deployed-SHA auf Server pruefen
ssh root@49.13.55.22 "cat /srv/rebreak/.last-deployed-sha"

# Health-Check
curl -sS -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/api/auth/me
# erwartet: 401