# 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:**
   ```bash
   # 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:
     ```bash
     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:**
   ```bash
   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

```bash
# 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
```
