name: Deploy # Warum diese Datei unter .github/workflows liegt und nicht unter .gitea/: # # Gitea Actions liest `.gitea/workflows`, **und nur wenn dieses Verzeichnis # fehlt**, ersatzweise `.github/workflows`. Ein neu angelegtes `.gitea/` # würde also ci.yml stillschweigend abschalten — der Lauf verschwände # einfach, ohne Fehlermeldung. Solange beide Dateien hier liegen, sieht Gitea # beide. (Auf GitHub bliebe dieser Workflow hängen: es gibt dort keinen # Runner mit dem Label `self-hosted`. Das Repository liegt aber ohnehin auf # git.elycon.solutions.) on: push: branches: [master] # Zwei Pushes kurz hintereinander sollen nicht zwei Deploys übereinander # fahren. Der laufende wird **nicht** abgebrochen — mitten im `docker compose # up` abgeschnitten zu werden ist der eine Zustand, den man nicht will. concurrency: group: deploy-${{ github.ref }} cancel-in-progress: false jobs: deploy: name: Migrationen und Container runs-on: self-hosted steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 # Nur was der Migrationsläufer braucht (`pg`), nicht der ganze Baum: # gebaut wird die Anwendung im Container, nicht hier. - name: Abhängigkeiten run: npm ci --omit=dev --ignore-scripts # ── .env aus den Gitea-Secrets ────────────────────────────────── # # Nicht die .env vom Server lesen: der Job läuft in einem eigenen # Container, und dessen Dateisystem ist nicht das des Hosts. Die Werte # kommen deshalb aus den Repository-Secrets und werden hier # zusammengesetzt. Sie landen in keinem Abbild — docker compose reicht # sie zur Laufzeit an den Container weiter. - name: .env schreiben env: DATABASE_URL: ${{ secrets.DATABASE_URL }} SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }} AUTH_SECRET: ${{ secrets.AUTH_SECRET }} AUTH_URL: ${{ secrets.AUTH_URL }} AUTH_MICROSOFT_ENTRA_ID_ID: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ID }} AUTH_MICROSOFT_ENTRA_ID_SECRET: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_SECRET }} AUTH_MICROSOFT_ENTRA_ID_ISSUER: ${{ secrets.AUTH_MICROSOFT_ENTRA_ID_ISSUER }} CRON_SECRET: ${{ secrets.CRON_SECRET }} run: | set -euo pipefail fehlend="" for name in DATABASE_URL SUPABASE_DB_URL AUTH_SECRET AUTH_URL \ AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \ AUTH_MICROSOFT_ENTRA_ID_ISSUER CRON_SECRET; do eval "wert=\${$name:-}" [ -n "$wert" ] || fehlend="$fehlend $name" done if [ -n "$fehlend" ]; then echo "Diese Secrets fehlen im Repository (Settings → Actions → Secrets):$fehlend" exit 1 fi # printf statt echo: ein Wert, der mit - beginnt, wäre sonst ein Schalter. { printf 'DATABASE_URL=%s\n' "$DATABASE_URL" printf 'AUTH_SECRET=%s\n' "$AUTH_SECRET" printf 'AUTH_URL=%s\n' "$AUTH_URL" printf 'AUTH_MICROSOFT_ENTRA_ID_ID=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_ID" printf 'AUTH_MICROSOFT_ENTRA_ID_SECRET=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_SECRET" printf 'AUTH_MICROSOFT_ENTRA_ID_ISSUER=%s\n' "$AUTH_MICROSOFT_ENTRA_ID_ISSUER" printf 'CRON_SECRET=%s\n' "$CRON_SECRET" } > .env chmod 600 .env # ── Migrationen ───────────────────────────────────────────────── # # Vor dem Neustart, nicht danach: der neue Code erwartet das neue # Schema. Umgekehrt liefe die neue Anwendung kurz gegen das alte und # fiele über fehlende Spalten. # # Erst zeigen, was ansteht — das steht dann im Protokoll des Laufs, auch # wenn danach etwas schiefgeht. - name: Ausstehende Migrationen zeigen env: SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }} run: node scripts/migrate.mjs --dry-run - name: Migrationen anwenden env: SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }} run: node scripts/migrate.mjs # ── Container ─────────────────────────────────────────────────── # # Läuft gegen den Docker-Dienst des Hosts (der Runner-Container hat # dessen Socket eingehängt). Der Projektname wird ausdrücklich gesetzt: # sonst leitet ihn Compose vom Verzeichnisnamen ab, und der ist im # Arbeitsverzeichnis des Runners ein anderer als bei der ersten # Installation von Hand — es entstünde ein zweiter Stapel daneben, # während der alte weiterläuft. - name: Bauen und starten env: COMPOSE_PROJECT_NAME: ${{ vars.COMPOSE_PROJECT_NAME || 'alpenwerk-hr' }} run: | set -euo pipefail docker compose build docker compose up -d --remove-orphans # ── Nachweis ──────────────────────────────────────────────────── # # Ohne diesen Schritt gilt ein Deploy als erfolgreich, sobald der # Container *gestartet* ist — auch wenn die Anwendung darin sofort # abstürzt. Gewartet wird auf den Healthcheck aus dem Dockerfile # (GET /login), nicht auf „läuft". - name: Warten, bis die Anwendung antwortet env: COMPOSE_PROJECT_NAME: ${{ vars.COMPOSE_PROJECT_NAME || 'alpenwerk-hr' }} run: | set -euo pipefail for versuch in $(seq 1 30); do zustand=$(docker compose ps --format '{{.Health}}' app | head -1) case "$zustand" in healthy) echo "Gesund nach $versuch Versuchen."; exit 0 ;; unhealthy) echo "Container meldet unhealthy."; break ;; esac sleep 4 done echo "Anwendung ist nicht gesund geworden. Letzte Ausgaben:" docker compose ps docker compose logs --tail 80 app exit 1 - name: Aufräumen if: always() run: rm -f .env