# CI/CD Deployment Workflow Diese Dokumentation beschreibt den kompletten Deployment-Workflow für das `rebreak-monorepo` nach der Migration weg von GitHub Actions hin zu einem selbstgehosteten Setup mit **Gitea** und **Woodpecker CI**. ## TL;DR - **Code-Hosting**: https://git.rebreak.org/chahine/rebreak-monorepo (Gitea) - **CI/CD**: https://ci.rebreak.org/chahine/rebreak-monorepo (Woodpecker) - **Build-Server**: `raynis-builder` / `api.trucko.org` / `128.140.47.53` - **Staging-Ziel**: `staging.rebreak.org` (`91.99.225.223`) - **Pipeline-Definition**: `.woodpecker.yml` im Repo - **Legacy-Fallback**: Gitea-Webhook → `scripts/deploy-webhook/server.mjs` → `scripts/deploy.sh` --- ## Infrastruktur-Übersicht ``` ┌─────────────────┐ push ┌──────────────────┐ │ Entwickler │ ────────────► │ Gitea │ │ (lokal) │ │ git.rebreak.org │ └─────────────────┘ └────────┬─────────┘ │ Webhook (push) │ Woodpecker CI │ (pull + build) ▼ ┌─────────────────────┐ │ Woodpecker Server │ │ ci.rebreak.org │ └──────────┬──────────┘ │ │ gRPC ▼ ┌─────────────────────┐ │ Woodpecker Agent │ │ raynis-builder │ │ 128.140.47.53 │ └──────────┬──────────┘ │ │ scp + ssh ▼ ┌─────────────────────┐ │ Staging-Server │ │ staging.rebreak.org│ └─────────────────────┘ ``` ### Server | Server | IP / Domain | Rolle | |--------|-------------|-------| | `raynis-builder` | `128.140.47.53`, `api.trucko.org` | Gitea, Woodpecker, Build-Agent | | `staging.rebreak.org` | `91.99.225.223` | Ziel für Staging-Deploys, Webhook-Listener | ### Dienste auf raynis-builder | Dienst | Container | Port (intern) | Extern | |--------|-----------|---------------|--------| | Gitea | `gitea` | `3000` | https://git.rebreak.org | | Woodpecker Server | `woodpecker-server` | `8000` | https://ci.rebreak.org | | Woodpecker Agent | `woodpecker-agent` | — | — | | Postgres | `gitea-db` | `5432` | — | ### Pfade auf raynis-builder - Gitea + Woodpecker Stack: `/mnt/HC_Volume_103985481/gitea/` - Deploy-SSH-Key: `/home/runner/.ssh/rebreak-deploy` - Gitea-Daten: `/mnt/HC_Volume_103985481/gitea/data/` - Woodpecker-Daten: `/mnt/HC_Volume_103985481/gitea/woodpecker-server/` --- ## Gitea ### Zugang - URL: https://git.rebreak.org - Admin-User: `chahine` - Repos: - `chahine/hello-ci` (Test-Repo) - `chahine/rebreak-monorepo` (Hauptrepo) ### SSH-Zugriff Gitea-SSH läuft auf **Port 2222**: ```bash git clone ssh://git@git.rebreak.org:2222/chahine/rebreak-monorepo.git ``` Für HTTPS: ```bash git clone https://git.rebreak.org/chahine/rebreak-monorepo.git ``` ### Deploy-Key - Auf dem Staging-Server liegt `/home/runner/.ssh/rebreak-deploy` - Der Public Key (`rebreak-deploy.pub`) ist in Gitea als Deploy-Key für `chahine/rebreak-monorepo` hinterlegt - Wird vom Staging-Server verwendet, um Code-Updates von Gitea zu pullen --- ## Woodpecker CI ### Zugang - URL: https://ci.rebreak.org - Login über Gitea-OAuth - Nur der Admin-User `chahine` ist automatisch Admin ### Pipeline-Definition Die Pipeline ist in `.woodpecker.yml` definiert: ```yaml when: - event: push branch: main - event: pull_request steps: install: image: node:24-slim commands: - corepack enable - corepack prepare pnpm@10.23.0 --activate - pnpm install --frozen-lockfile build-backend: image: node:24-slim commands: - corepack enable - corepack prepare pnpm@10.23.0 --activate - cd backend && NODE_OPTIONS=--max-old-space-size=4096 pnpm build depends_on: [install] build-admin: image: node:24-slim commands: - corepack enable - corepack prepare pnpm@10.23.0 --activate - cd apps/admin && pnpm build depends_on: [install] deploy-backend: image: alpine:3.21 commands: - apk add --no-cache openssh-client - mkdir -p ~/.ssh - cp /root/ssh-keys/rebreak-deploy ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H staging.rebreak.org > ~/.ssh/known_hosts 2>/dev/null - tar czf backend-output.tar.gz -C backend/.output . - scp -i ~/.ssh/id_ed25519 backend-output.tar.gz root@staging.rebreak.org:/srv/rebreak/backend/.output-incoming.tar.gz - ssh -i ~/.ssh/id_ed25519 root@staging.rebreak.org 'bash /srv/rebreak/scripts/deploy-from-artifact.sh' depends_on: [build-backend] when: - event: push branch: main deploy-admin: image: alpine:3.21 commands: - apk add --no-cache openssh-client - mkdir -p ~/.ssh - cp /root/ssh-keys/rebreak-deploy ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H staging.rebreak.org > ~/.ssh/known_hosts 2>/dev/null - tar czf admin-output.tar.gz -C apps/admin/.output . - scp -i ~/.ssh/id_ed25519 admin-output.tar.gz root@staging.rebreak.org:/srv/rebreak/apps/admin/.output-incoming.tar.gz - ssh -i ~/.ssh/id_ed25519 root@staging.rebreak.org 'bash /srv/rebreak/scripts/deploy-admin-from-artifact.sh' depends_on: [build-admin] when: - event: push branch: main ``` ### Wichtige Details - **Build** läuft in `node:24-slim`-Containern auf dem Woodpecker-Agent. - **Deploy** läuft in `alpine:3.21`-Containern. - Der SSH-Key wird über `WOODPECKER_BACKEND_DOCKER_VOLUMES` in jeden Pipeline-Container gemountet: - Host-Pfad: `/home/runner/.ssh` - Container-Pfad: `/root/ssh-keys` - Deploy-Steps laufen nur bei `push` auf `main`. - Pull Requests werden gebaut, aber nicht deployed. ### Agent-Konfiguration Wichtige Env-Variablen in `/mnt/HC_Volume_103985481/gitea/docker-compose.yml`: ```yaml woodpecker-agent: environment: - WOODPECKER_SERVER=woodpecker-server:9000 - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} - WOODPECKER_BACKEND_DOCKER_VOLUMES=/home/runner/.ssh:/root/ssh-keys:ro ``` `WOODPECKER_BACKEND_DOCKER_VOLUMES` mountet den Host-SSH-Key in **alle** Pipeline-Container. --- ## Staging-Deploy-Mechanismus ### Primärer Pfad: Woodpecker CI 1. Push auf `main` in Gitea 2. Gitea benachrichtigt Woodpecker 3. Woodpecker startet die Pipeline 4. Pipeline baut Backend und Admin 5. Pipeline kopiert Artifacts per `scp` auf `staging.rebreak.org` 6. Auf dem Staging-Server werden diese Scripts ausgeführt: - `/srv/rebreak/scripts/deploy-from-artifact.sh` (Backend) - `/srv/rebreak/scripts/deploy-admin-from-artifact.sh` (Admin) 7. Die Scripts extrahieren das Artifact und starten die pm2-Prozesse neu ### Legacy-Fallback: Gitea-Webhook Ein zusätzlicher Webhook in Gitea triggert den alten Webhook-Listener auf dem Staging-Server: - **Webhook-URL**: `https://staging.rebreak.org/webhook` - **Listener**: `scripts/deploy-webhook/server.mjs` (Port 9000, reverse-proxied via Nginx) - **Deploy-Script**: `scripts/deploy.sh` - **Funktion**: - Validiert HMAC-SHA256-Signatur - Prüft, ob Branch `main` ist - Startet `scripts/deploy.sh` - Führt `git pull`, `pnpm install`, `prisma migrate deploy` (falls nötig), Build und pm2-Restart aus #### Warum zwei Pfade? - **Woodpecker** ist der bevorzugte Pfad: Build auf leistungsfähigem Server, Artifact-Deploy. - **Webhook** ist ein Legacy-Fallback, der z.B. Migrationen direkt auf dem Staging-Server ausführt. - Der `.deploy-ga.lock` verhindert, dass beide gleichzeitig laufen. #### Signatur-Validierung Der Listener unterstützt beide Formate: - **GitHub**: `x-hub-signature-256: sha256=...` - **Gitea**: `x-gitea-signature: ...` (nur Hex, kein `sha256=` Präfix) Secret: `GITHUB_WEBHOOK_SECRET` aus `/etc/environment` auf dem Staging-Server. --- ## SSH-Key / Secrets ### `rebreak-deploy` Key - **Privater Key**: `/home/runner/.ssh/rebreak-deploy` auf raynis-builder - **Public Key**: `/home/runner/.ssh/rebreak-deploy.pub` - **Fingerprint**: `SHA256:Wkw1O4YGEM9q++dbCd3+CjAfILtjhPasE71wG5wnH4Q` - **Verwendung**: - Woodpecker-Deploy-Steps verwenden ihn für `scp`/`ssh` zum Staging-Server - Staging-Server pullt damit Code von Gitea (Deploy-Key im Repo) - **Auf Staging-Server authorisiert**: Ja, in `~root/.ssh/authorized_keys` ### Gitea-OAuth-App für Woodpecker - Name: `Woodpecker CI` - Type: `confidential_client` - Redirect URI: `https://ci.rebreak.org/authorize` - Scopes: Standard (keine expliziten nötig) ### Woodpecker Secrets - `staging_deploy_key` wurde ursprünglich in Woodpecker hinterlegt, wird aber **nicht mehr verwendet**. - Der Key wird stattdessen über `WOODPECKER_BACKEND_DOCKER_VOLUMES` gemountet. --- ## Wichtige Dateien | Datei | Zweck | |-------|-------| | `.woodpecker.yml` | Pipeline-Definition für Woodpecker | | `scripts/deploy-webhook/server.mjs` | Webhook-Listener auf Staging-Server | | `scripts/deploy.sh` | Legacy Deploy-Script (inkl. Prisma-Migrationen) | | `scripts/deploy-from-artifact.sh` | Artifact-Deploy für Backend | | `scripts/deploy-admin-from-artifact.sh` | Artifact-Deploy für Admin | | `ecosystem.config.js` | pm2-Konfiguration auf Staging-Server | | `/mnt/HC_Volume_103985481/gitea/docker-compose.yml` | Gitea + Woodpecker Stack | --- ## Prisma-Migrationen Migrationen werden an zwei Stellen geprüft: 1. **In `scripts/deploy-from-artifact.sh`** (Woodpecker-Deploy): - Vergleicht `.last-deployed-sha` mit HEAD - Führt `prisma migrate deploy` aus, wenn sich etwas unter `backend/prisma/migrations/` oder `backend/prisma/schema.prisma` geändert hat 2. **In `scripts/deploy.sh`** (Webhook-Deploy): - Gleicher Mechanismus - Wird als Fallback ausgeführt > ⚠️ Wichtig: Migrationen werden **nur** ausgeführt, wenn sich die Prisma-Dateien seit dem letzten Deploy geändert haben. Bei manuellem Eingriff muss ggf. `.last-deployed-sha` gelöscht werden. --- ## Betrieb: Pipelines ansehen ### Woodpecker-Logs ```bash ssh root@128.140.47.53 docker logs woodpecker-server --tail 100 docker logs woodpecker-agent --tail 100 ``` ### Pipeline-Status in DB ```bash sqlite3 /mnt/HC_Volume_103985481/gitea/woodpecker-server/woodpecker.sqlite \ "SELECT id, repo_id, number, status, message FROM pipelines WHERE repo_id=2 ORDER BY id DESC LIMIT 5;" ``` ### Webhook-Logs auf Staging ```bash ssh root@staging.rebreak.org pm2 logs rebreak-webhook --lines 50 ``` --- ## Troubleshooting ### Pipeline-Clone schlägt fehl mit Submodule-Fehler Ursache: Verwaister Submodule-Eintrag ohne `.gitmodules`. Lösung: ```bash git rm --cached echo '/' >> .gitignore git commit -m "Remove broken submodule entry" ``` ### Deploy-Step: "Load key ... error in libcrypto" Ursache: Alpine-OpenSSH konnte das Key-Format nicht lesen. Lösung: `image: alpine:3.21` statt `image: alpine` verwenden. ### "Permission denied (publickey)" beim Deploy Ursache: SSH-Key nicht im Container verfügbar oder nicht auf Staging authorisiert. Prüfung: ```bash # Ist der Key auf Staging authorisiert? ssh -i /home/runner/.ssh/rebreak-deploy root@staging.rebreak.org 'echo OK' # Ist WOODPECKER_BACKEND_DOCKER_VOLUMES gesetzt? docker inspect woodpecker-agent --format '{{json .Config.Env}}' ``` ### Webhook gibt 401 Ursache: Signatur stimmt nicht. Prüfung: - Ist `GITHUB_WEBHOOK_SECRET` in `/etc/environment` auf Staging gesetzt? - Stimmt das Secret im Gitea-Webhook? - Verwendet Gitea den richtigen Signatur-Header? Der Listener akzeptiert sowohl `x-hub-signature-256` (GitHub) als auch `x-gitea-signature` (Gitea). ### Woodpecker zeigt keine Repos Ursache: OAuth-App falsch konfiguriert oder User nicht angemeldet. Lösung: - Auf https://ci.rebreak.org ausloggen und wieder einloggen - In Gitea unter Settings → Applications prüfen, ob die OAuth-App existiert - OAuth-App muss `confidential_client: true` sein ### Zwei Deploys kollidieren Der `.deploy-ga.lock` verhindert parallele Deploys. Wenn ein Deploy hängt: ```bash ssh root@staging.rebreak.org rm -f /srv/rebreak/.deploy-ga.lock ``` --- ## Migration von GitHub ### Was wurde gemacht? 1. Repo auf Gitea erstellt 2. Code von GitHub nach Gitea gepusht 3. Woodpecker mit Gitea verbunden 4. `.woodpecker.yml` erstellt 5. Staging-Server remote auf Gitea umgestellt 6. Gitea-Webhook für Legacy-Deploy eingerichtet 7. Webhook-Listener für Gitea-Signaturen angepasst ### Was noch zu tun ist? - Lokale Entwickler-Repos sollten den `origin`-Remote auf Gitea umstellen: ```bash git remote set-url origin https://git.rebreak.org/chahine/rebreak-monorepo.git # oder SSH: git remote set-url origin ssh://git@git.rebreak.org:2222/chahine/rebreak-monorepo.git ``` - Falls GitHub komplett abgeschaltet werden soll, den GitHub-Remote entfernen und keinen `git push origin main` mehr ausführen. - Der `staging_deploy_key`-Secret in Woodpecker kann gelöscht werden (wird nicht mehr verwendet). --- ## Kontakte / Verantwortlichkeiten - Infrastruktur: `raynis-builder` (Hetzner VPS) - Gitea / Woodpecker Admin: `chahine` - Staging-Server: `staging.rebreak.org` --- *Letzte Aktualisierung: 2026-06-18*