name: EK-DOS-WEB bauen und ausrollen

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

# Zwei gleichzeitige Ausrollvorgänge kämen sich beim Symlink-Tausch in die Quere.
# Ein neuer Lauf wartet, statt den laufenden abzubrechen.
concurrency:
  group: ekdos-deploy
  cancel-in-progress: false

env:
  DEPLOY_HOST: 10.0.11.131
  DEPLOY_USER: deploy
  DEPLOY_PATH: /var/www/ekdos

jobs:
  # ──────────────────────────────────────────────────────────────────────────
  # Backend: nichts zu kompilieren, aber alles zu prüfen. Composer läuft hier
  # NICHT -- die Abhängigkeiten werden auf dem Webserver aufgelöst, wo PHP-
  # Version und Erweiterungen tatsächlich stimmen. Eine Plattformprüfung im
  # Runner würde nur sich selbst prüfen.
  # ──────────────────────────────────────────────────────────────────────────
  backend-pruefen:
    runs-on: ubuntu-latest
    container: php:8.5-cli
    steps:
      # MUSS vor dem Checkout stehen.
      #
      # actions/checkout ist eine JavaScript-Aktion. act_runner führt solche
      # Aktionen mit dem `node` AUS DEM JOB-CONTAINER aus -- anders als die
      # gehosteten Runner von GitHub, die ihr eigenes node mitbringen.
      # php:8.5-cli enthält weder node noch git, deshalb scheitert der Checkout
      # dort mit „exit code 127" (command not found), ohne eine Zeile Ausgabe.
      #
      # Dieser Schritt ist ein reines Shell-Kommando und braucht kein node,
      # läuft also auch im nackten Container.
      - name: git und node für den Checkout bereitstellen
        run: |
          set -eu
          apt-get update
          apt-get install -y --no-install-recommends git nodejs curl ca-certificates
          echo "node $(node --version), $(git --version)"

      - uses: actions/checkout@v4

      - name: Syntax aller PHP-Dateien prüfen
        working-directory: backend
        run: |
          set -eu
          find src public bin -type f \( -name '*.php' -o -name 'ekdos' \) -print0 \
            | xargs -0 -n1 -P4 php -l

      - name: composer.json und composer.lock prüfen
        working-directory: backend
        run: |
          set -eu
          curl -sS https://getcomposer.org/installer \
            | php -- --install-dir=/usr/local/bin --filename=composer

          # Ohne Sperrdatei löst der Webserver bei jedem Ausrollen neu auf und
          # bekommt womöglich andere Fassungen als hier geprüft.
          test -f composer.lock || {
            echo "composer.lock fehlt."
            echo
            echo "Einmalig auf dem Webserver erzeugen -- nur dort stimmen PHP-Fassung"
            echo "und Erweiterungen, sodass die Auflösung zur Zielmaschine passt:"
            echo
            echo "  scp backend/composer.json deploy@10.0.11.131:/tmp/"
            echo "  ssh deploy@10.0.11.131 'cd /tmp && composer update --no-interaction --no-autoloader --no-scripts'"
            echo "  scp deploy@10.0.11.131:/tmp/composer.lock backend/"
            echo
            echo "Danach backend/composer.lock einchecken."
            exit 1
          }

          # --strict, damit eine veraltete Sperrdatei den Lauf anhält statt nur
          # zu warnen: sonst rollt der Server etwas anderes aus als hier geprüft.
          # (composer validate meldet das ohne --strict nur als Warnung.)
          composer validate --no-check-publish --strict

  # ──────────────────────────────────────────────────────────────────────────
  # Bauen und ausrollen in einem Auftrag.
  #
  # Bewusst ohne upload-/download-artifact: welche Fassung dieser Aktionen eine
  # Gitea-Instanz beherrscht (v3 oder v4), hängt an deren Version und ist eine
  # verlässliche Quelle für Fehlschläge beim ersten Lauf. Das Bündel bleibt
  # einfach im Arbeitsverzeichnis liegen und geht von dort per rsync raus.
  #
  # Node existiert nur in diesem Container -- auf dem Webserver läuft keines.
  # ──────────────────────────────────────────────────────────────────────────
  bauen-und-ausrollen:
    runs-on: ubuntu-latest
    container: node:22-bookworm
    needs: [backend-pruefen]
    steps:
      - uses: actions/checkout@v4

      - name: pnpm bereitstellen
        run: corepack enable && corepack prepare pnpm@10.16.1 --activate

      - name: Abhängigkeiten installieren
        working-directory: frontend
        run: pnpm install --frozen-lockfile

      # Getrennt von `build`, damit ein Typfehler als solcher im Protokoll steht
      # und nicht als Bündelfehler.
      - name: Typen prüfen
        working-directory: frontend
        run: pnpm typecheck

      - name: Bündel bauen
        working-directory: frontend
        run: pnpm build

      - name: Bündel prüfen
        working-directory: frontend
        run: |
          set -eu
          test -f dist/index.html || { echo "dist/index.html fehlt -- der Bau hat nichts erzeugt."; exit 1; }
          du -sh dist

      # ── Ab hier nur noch aus main ──────────────────────────────────────────

      - name: Werkzeuge zum Ausrollen installieren
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          apt-get update
          apt-get install -y --no-install-recommends rsync openssh-client

      - name: SSH einrichten
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        env:
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
        run: |
          set -eu
          test -n "${DEPLOY_SSH_KEY:-}" || { echo "Secret DEPLOY_SSH_KEY fehlt."; exit 1; }

          install -m 700 -d ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

          # Bevorzugt der hinterlegte Fingerabdruck. Fehlt er, wird er erfragt --
          # bequem, aber ohne Schutz vor einem untergeschobenen Ziel. Für den
          # Dauerbetrieb setzen:  ssh-keyscan -t ed25519 10.0.11.131
          if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then
            printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
          else
            echo "Hinweis: DEPLOY_KNOWN_HOSTS ist nicht gesetzt, Fingerabdruck wird ungeprüft übernommen."
            ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
          fi
          chmod 600 ~/.ssh/known_hosts

      # Lieber hier scheitern als nach dem halben Hochladen.
      - name: Ziel prüfen
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          ssh "$DEPLOY_USER@$DEPLOY_HOST" "
            set -eu
            test -d '$DEPLOY_PATH/releases'   || { echo '$DEPLOY_PATH/releases fehlt -- siehe docs/DEPLOYMENT.md'; exit 1; }
            test -f '$DEPLOY_PATH/shared/.env' || { echo '$DEPLOY_PATH/shared/.env fehlt -- einmalig anlegen'; exit 1; }
            command -v composer >/dev/null    || { echo 'composer fehlt auf dem Webserver'; exit 1; }
            command -v php      >/dev/null    || { echo 'php fehlt auf dem Webserver'; exit 1; }
            # Prüft die sudo-Regel, ohne etwas auszuführen.
            sudo -n -l /bin/systemctl reload php8.5-fpm >/dev/null 2>&1 \
              || echo 'Hinweis: sudo-Regel für den fpm-Neustart fehlt (siehe docs/DEPLOYMENT.md) -- release.sh wird daran scheitern.'
            mkdir -p '$DEPLOY_PATH/releases/${{ github.sha }}' '$DEPLOY_PATH/deploy'
          "

      - name: Oberfläche übertragen
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          rsync -az --delete \
            frontend/dist/ \
            "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/public/"

      - name: Backend übertragen
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          # vendor/ entsteht auf dem Server; .env liegt in shared/ und wird dort
          # verknüpft; var/ ist der Zwischenspeicher dieser Veröffentlichung.
          rsync -az --delete \
            --exclude 'vendor/' \
            --exclude '.env' \
            --exclude 'var/' \
            backend/ \
            "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/backend/"

      - name: Ausrollskripte übertragen
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          rsync -az --chmod=F755 \
            deploy/release.sh deploy/rollback.sh \
            "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deploy/"

      # Hier passiert das Eigentliche: composer install, Migration, atomarer
      # Symlink-Tausch, php-fpm neu laden, n8n-Cache verwerfen,
      # Gesundheitsprüfung. Das Skript liegt unter deploy/release.sh und lässt
      # sich genauso von Hand aufrufen.
      - name: Veröffentlichung aktivieren
        id: aktivieren
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          ssh "$DEPLOY_USER@$DEPLOY_HOST" \
            "$DEPLOY_PATH/deploy/release.sh '${{ github.sha }}'"

      # KEIN automatischer Rückschritt.
      #
      # release.sh tauscht den Symlink erst nach composer und Migration. Scheitert
      # es davor, läuft die alte Fassung unverändert weiter -- ein Rückschritt
      # würde dann auf die *vorletzte* schalten und aus einem folgenlosen
      # Fehlschlag einen Ausfall machen. Deshalb hier nur berichten, was live ist.
      - name: Bei Fehlschlag Lage berichten
        if: failure() && steps.aktivieren.outcome == 'failure'
        run: |
          echo "Das Aktivieren ist fehlgeschlagen. Aktueller Stand auf dem Server:"
          ssh "$DEPLOY_USER@$DEPLOY_HOST" "
            echo -n '  aktiv:  '; readlink '$DEPLOY_PATH/current' || echo '(keiner)'
            echo -n '  vorher: '; cat '$DEPLOY_PATH/previous' 2>/dev/null || echo '(nichts vermerkt)'
            curl -fsS --max-time 5 http://127.0.0.1/api/health && echo || echo '  /api/health antwortet NICHT'
          " || true
          echo
          echo "Läuft die alte Fassung noch, ist nichts weiter zu tun -- Ursache beheben und erneut pushen."
          echo "Sonst von Hand zurück:  ssh $DEPLOY_USER@$DEPLOY_HOST $DEPLOY_PATH/deploy/rollback.sh"
          exit 1

      # /api/health ist im Port-80-Block von der HTTPS-Umleitung ausgenommen,
      # sonst wäre die 301 für `curl -f` ein Erfolg und PHP nie erreicht.
      - name: Ergebnis
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          set -eu
          echo "Ausgerollt: ${{ github.sha }}"
          ssh "$DEPLOY_USER@$DEPLOY_HOST" \
            "curl -fsS --max-time 5 -H 'Accept: application/json' http://127.0.0.1/api/health"
          echo
