diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..03585ec --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,117 @@ +name: Deploy auf den Timer-Server + +# Laeuft bei jedem Push auf main und laesst sich in der Gitea-Oberflaeche +# ("Actions" -> Workflow -> "Run workflow") auch von Hand ausloesen. +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + env: + # Repository-Variablen (Settings -> Actions -> Variables) + DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} + DEPLOY_USER: ${{ vars.DEPLOY_USER }} + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + DEPLOY_SERVICE: ${{ vars.DEPLOY_SERVICE }} + DEPLOY_OWNER: ${{ vars.DEPLOY_OWNER }} + HEALTH_URL: ${{ vars.HEALTH_URL }} + # Repository-Secrets (Settings -> Actions -> Secrets) + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + + steps: + - name: Code auschecken + uses: actions/checkout@v4 + + - name: Werkzeuge installieren + run: | + apt-get update -qq + apt-get install -y -qq rsync openssh-client + + - name: Konfiguration pruefen + run: | + fehlt="" + for v in DEPLOY_HOST DEPLOY_USER DEPLOY_PATH DEPLOY_SERVICE DEPLOY_SSH_KEY DEPLOY_KNOWN_HOSTS; do + if [ -z "${!v:-}" ]; then fehlt="$fehlt $v"; fi + done + if [ -n "$fehlt" ]; then + echo "Es fehlen noch Variablen/Secrets:$fehlt" + echo "Nachzutragen unter Settings -> Actions -> Variables bzw. Secrets." + exit 1 + fi + echo "Ziel: $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH (Dienst: $DEPLOY_SERVICE)" + + - name: SSH einrichten + run: | + mkdir -p ~/.ssh && chmod 700 ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts + chmod 600 ~/.ssh/id_ed25519 + chmod 644 ~/.ssh/known_hosts + + - name: Dateien uebertragen + run: | + # --delete raeumt geloeschte Dateien auf dem Server weg. Die + # ausgeschlossenen Pfade sind davon ausgenommen (kein --delete-excluded!): + # Konfiguration, Datenbank und node_modules bleiben unangetastet. + rsync -rlpt --delete --out-format='%n' \ + --exclude='.git/' --exclude='.gitea/' \ + --exclude='.env' --exclude='.env.*' \ + --exclude='data/' --exclude='node_modules/' \ + -e 'ssh -o BatchMode=yes' \ + ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" | tee uebertragen.txt + + - name: Abhaengigkeiten, Neustart und Health-Check + run: | + # Neu installiert wird nur, wenn sich die Paketliste geaendert hat. + # bcrypt und sqlite3 sind native Module: npm ci gehoert deshalb auf + # den Zielhost und nicht in den Runner-Container. + NPM=nein + RESTART=nein + if grep -q 'backend/package' uebertragen.txt; then NPM=ja; fi + if grep -qE '^(backend/|deploy/)' uebertragen.txt; then RESTART=ja; fi + if [ ! -s uebertragen.txt ]; then echo "Nichts geaendert."; fi + echo "npm ci: $NPM | Neustart: $RESTART" + + ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ + "NPM='$NPM' RESTART='$RESTART' \ + DEPLOY_PATH='$DEPLOY_PATH' DEPLOY_SERVICE='$DEPLOY_SERVICE' \ + DEPLOY_OWNER='${DEPLOY_OWNER:-timerapp:timerapp}' \ + HEALTH_URL='${HEALTH_URL:-http://127.0.0.1:3003/health}' \ + bash -euo pipefail -s" <<'REMOTE' + SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n" + + if [ "$NPM" = ja ]; then + echo "--- npm ci (nur Produktionsabhaengigkeiten)" + cd "$DEPLOY_PATH/backend" + $SUDO npm ci --omit=dev --no-audit --no-fund + fi + + # Alles unter dem Anwendungsverzeichnis gehoert dem Dienstbenutzer. + $SUDO chown -R "$DEPLOY_OWNER" "$DEPLOY_PATH" + + if [ "$RESTART" = nein ]; then + echo "--- Keine Backend-Aenderung: Neustart nicht noetig" + exit 0 + fi + + echo "--- Dienst neu starten: $DEPLOY_SERVICE" + $SUDO systemctl restart "$DEPLOY_SERVICE" + + echo "--- Health-Check" + for i in $(seq 1 15); do + if curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then + echo "Dienst antwortet:"; curl -fsS "$HEALTH_URL"; echo + exit 0 + fi + sleep 2 + done + + echo "Dienst antwortet nach 30 s nicht. Letzte Zeilen aus dem Journal:" + $SUDO journalctl -u "$DEPLOY_SERVICE" -n 40 --no-pager || true + exit 1 + REMOTE diff --git a/DEPLOY.md b/DEPLOY.md index c40e884..d6b21e6 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -475,3 +475,73 @@ CSV, Filter, Löschen, Abmelden) sowie ein Durchlauf im echten Browser. Die App ist klein genug, um sie vollständig zu lesen: `server.js` ~470 Zeilen, `app.js` ~460 Zeilen, beide durchgehend kommentiert und auf Deutsch beschriftet. Im Zweifel ist das Lesen der Quelle schneller als das Raten. + +--- + +## Automatischer Deploy per Gitea Actions + +Jeder Push auf `main` in `git.handy-insel.de/kerimatilgan/timer-app` rollt den +Stand selbsttaetig auf den Server aus. Der Workflow liegt in +`.gitea/workflows/deploy.yaml` und laesst sich in der Gitea-Oberflaeche unter +"Actions" auch von Hand starten. + +### Was der Workflow tut + +1. Uebertraegt die versionierten Dateien per `rsync` nach `/opt/timer-app`. + **Nicht angefasst** werden `backend/.env`, `data/` und `node_modules/` — + sie sind vom Abgleich ausgenommen und ueberleben auch das `--delete`, + das geloeschte Dateien auf dem Server aufraeumt. +2. `npm ci --omit=dev` **nur**, wenn sich `backend/package*.json` geaendert hat. + Das laeuft bewusst auf dem Zielhost: `bcrypt` und `sqlite3` sind native + Module und muessen zur dortigen Architektur passen. +3. Neustart des Dienstes **nur**, wenn sich etwas unter `backend/` oder + `deploy/` geaendert hat. Reine Frontend-Aenderungen brauchen keinen. +4. Health-Check gegen `/health` (15 Versuche, 30 s). Antwortet der Dienst + nicht, schlaegt der Lauf fehl und die letzten 40 Journal-Zeilen stehen + im Protokoll. + +### Einmalige Einrichtung + +Auf dem Zielhost einen Deploy-Benutzer anlegen, ihm das Verzeichnis geben und +den Neustart des einen Dienstes erlauben: + +```bash +useradd -r -m -s /bin/bash deploy +mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh +echo '' >> /home/deploy/.ssh/authorized_keys +chown -R deploy:deploy /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys + +# Schreibrecht auf das Anwendungsverzeichnis +chown -R deploy:timerapp /opt/timer-app && chmod -R g+rX /opt/timer-app + +# Genau die drei Befehle ohne Passwort - nicht mehr +cat > /etc/sudoers.d/deploy-timer-app <<'EOF' +deploy ALL=(root) NOPASSWD: /bin/systemctl restart timer-app, \ + /bin/journalctl -u timer-app *, \ + /bin/chown -R timerapp\:timerapp /opt/timer-app +EOF +chmod 440 /etc/sudoers.d/deploy-timer-app +visudo -c +``` + +Im Repo unter **Settings -> Actions** hinterlegen: + +| Art | Name | Wert | +|---|---|---| +| Variable | `DEPLOY_HOST` | `192.168.1.5` | +| Variable | `DEPLOY_USER` | `deploy` | +| Variable | `DEPLOY_PATH` | `/opt/timer-app` | +| Variable | `DEPLOY_SERVICE` | `timer-app` | +| Variable | `DEPLOY_OWNER` | `timerapp:timerapp` | +| Variable | `HEALTH_URL` | `http://127.0.0.1:3003/health` | +| Secret | `DEPLOY_SSH_KEY` | privater Deploy-Schluessel (ganze Datei inkl. BEGIN/END-Zeilen) | +| Secret | `DEPLOY_KNOWN_HOSTS` | `ssh-keyscan 192.168.1.5` — Zeile(n) fuer den Zielhost | + +### Grenzen + +- Die `.env` wird **nie** ausgerollt. Kommt eine neue Einstellung dazu, gehoert + sie von Hand in die `.env` auf dem Server (Vorlage: `backend/.env.example`), + sonst startet der Dienst nach dem Deploy womoeglich nicht. +- Es gibt keinen automatischen Rollback. Faellt der Health-Check durch, laeuft + der Dienst weiter im Fehlerzustand — der Lauf ist rot und das Journal steht + im Protokoll. Zurueck geht es mit einem Revert-Commit auf `main`.