diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 373b998..9ba33fa 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -1,7 +1,7 @@ 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. +# ("Actions" -> Workflow -> "Run workflow") auch von Hand starten. on: push: branches: [main] @@ -10,32 +10,28 @@ on: jobs: deploy: # Nicht ubuntu-latest (24.04): in dem Image scheitern auf diesem Runner - # moderne Syscalls - actions/checkout stirbt mit "read ENOTCONN", rsync - # mit "pipe: Permission denied". Ursache ist das seccomp-Profil des - # Docker-Hosts, nicht der Workflow. Das aeltere Image kommt damit klar. + # moderne Syscalls - actions/checkout stirbt mit "read ENOTCONN". + # Ursache ist das seccomp-Profil des Docker-Hosts, nicht der Workflow. runs-on: ubuntu-22.04 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 }} 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: # Bewusst kein actions/checkout: die JS-Aktion stirbt im Runner-Image - # von Gitea 1.27 beim Aufruf von git mit "read ENOTCONN" (Node-24-Fehler - # im Container). Ein Klon per Shell umgeht das und braucht kein Node. + # beim Aufruf von git mit "read ENOTCONN". Ein Klon per Shell umgeht das. - name: Werkzeuge und Code holen env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | apt-get update -qq - apt-get install -y -qq git rsync openssh-client curl + apt-get install -y -qq git openssh-client curl rm -rf quelle && mkdir quelle && cd quelle git init -q @@ -43,7 +39,6 @@ jobs: # deshalb Schema und Rest getrennt behandeln statt https anzunehmen. URL="${GITHUB_SERVER_URL%/}" git remote add origin "${URL%%://*}://x-access-token:$TOKEN@${URL#*://}/$GITHUB_REPOSITORY.git" - # Nach SHA, falls der Server das nicht erlaubt ersatzweise nach Branch. git fetch -q --depth 1 origin "$GITHUB_SHA" || git fetch -q --depth 1 origin "$GITHUB_REF_NAME" git checkout -q FETCH_HEAD echo "Ausgecheckt: $(git log --oneline -1)" @@ -69,47 +64,65 @@ jobs: chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/known_hosts - - name: Verbindung pruefen + - name: Zielhost pruefen run: | ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" "DEPLOY_PATH='$DEPLOY_PATH' bash -s" <<'PRUEF' echo "Angemeldet als $(id -un) auf $(hostname)" - for w in rsync npm node systemctl; do + for w in tar npm node systemctl curl; do printf ' %-10s %s\n' "$w" "$(command -v $w || echo FEHLT)" done echo "Rechte:" ls -ld "$DEPLOY_PATH" "$DEPLOY_PATH/backend" "$DEPLOY_PATH/data" 2>&1 | sed 's/^/ /' - echo "Schreibtest im Anwendungsverzeichnis:" if touch "$DEPLOY_PATH/.schreibtest" 2>/dev/null; then - rm -f "$DEPLOY_PATH/.schreibtest"; echo " ok" + rm -f "$DEPLOY_PATH/.schreibtest"; echo "Schreibtest: ok" else - echo " FEHLGESCHLAGEN - der Deploy-Benutzer darf dort nicht schreiben" + echo "Schreibtest: FEHLGESCHLAGEN - $(id -un) darf in $DEPLOY_PATH nicht schreiben"; exit 1 fi - echo "sudo-Rechte:"; sudo -n -l 2>&1 | tail -5 | sed 's/^/ /' + echo "sudo-Rechte:"; sudo -n -l 2>&1 | tail -4 | sed 's/^/ /' PRUEF - - name: Dateien uebertragen und abgleichen + - name: Aenderungen gegen den ausgerollten Stand ermitteln run: | - # rsync laeuft NICHT im Runner-Container: dessen Docker-Host verbietet - # den pipe()-Aufruf ("pipe: Permission denied"). Deshalb geht das - # Paket per tar hinueber und der Abgleich passiert auf dem Zielhost. + # Der zuletzt ausgerollte Commit liegt als Merkzettel auf dem Server. + # Daraus wird die Dateiliste bestimmt - genauer als ein Zeitstempel- + # vergleich und ohne rsync, das es auf dem Zielhost nicht gibt. + ALT=$(ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ + "cat '$DEPLOY_PATH/.deploy-commit' 2>/dev/null || true") + cd quelle + + if [ -n "$ALT" ] && (git cat-file -e "${ALT}^{commit}" 2>/dev/null || git fetch -q --depth 1 origin "$ALT" 2>/dev/null); then + echo "Vorheriger Stand: $ALT" + git diff --name-status "$ALT" "$GITHUB_SHA" > ../aenderungen.txt || true + else + echo "Kein bekannter Vorgaengerstand - alle Dateien gelten als neu." + git ls-files | sed 's/^/A\t/' > ../aenderungen.txt + fi + + cd .. + echo "--- Aenderungen ---"; cat aenderungen.txt + + - name: Dateien uebertragen + run: | + # Das Paket enthaelt ausschliesslich versionierte Dateien. Die .env, + # data/ und node_modules/ sind darin gar nicht vorhanden und koennen + # deshalb auch nicht ueberschrieben werden. tar -czf paket.tgz -C quelle --exclude=.git --exclude=.gitea . ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ - 'rm -rf /tmp/timer-app-release && mkdir -p /tmp/timer-app-release && tar -xzf - -C /tmp/timer-app-release' \ - < paket.tgz + "tar -xzf - -C '$DEPLOY_PATH'" < paket.tgz + echo "Paket entpackt." - # --delete raeumt geloeschte Dateien weg. Die ausgeschlossenen Pfade - # sind davon ausgenommen (kein --delete-excluded!): Konfiguration, - # Datenbank und node_modules bleiben unangetastet. + # Im Repo geloeschte Dateien auch auf dem Server entfernen. + GELOESCHT=$(awk '$1 ~ /^D/ {print $2}' aenderungen.txt) + if [ -n "$GELOESCHT" ]; then + echo "Entferne:"; echo "$GELOESCHT" | sed 's/^/ /' + echo "$GELOESCHT" | ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ + "cd '$DEPLOY_PATH' && xargs -r rm -f --" + fi + + # Merkzettel fortschreiben - Grundlage fuer den naechsten Lauf. ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ - "DEPLOY_PATH='$DEPLOY_PATH' bash -euo pipefail -s" > uebertragen.txt <<'SYNC' - rsync -rlpt --delete --out-format='%n' \ - --exclude='.env' --exclude='.env.*' \ - --exclude='data/' --exclude='node_modules/' \ - /tmp/timer-app-release/ "$DEPLOY_PATH/" - rm -rf /tmp/timer-app-release - SYNC - cat uebertragen.txt + "echo '$GITHUB_SHA' > '$DEPLOY_PATH/.deploy-commit'" - name: Abhaengigkeiten, Neustart und Health-Check run: | @@ -118,9 +131,9 @@ jobs: # 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 + if awk '{print $2}' aenderungen.txt | grep -q '^backend/package'; then NPM=ja; fi + if awk '{print $2}' aenderungen.txt | grep -qE '^(backend/|deploy/)'; then RESTART=ja; fi + if [ ! -s aenderungen.txt ]; then echo "Nichts geaendert."; fi echo "npm ci: $NPM | Neustart: $RESTART" ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \ @@ -128,12 +141,10 @@ jobs: DEPLOY_PATH='$DEPLOY_PATH' DEPLOY_SERVICE='$DEPLOY_SERVICE' \ 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 + npm ci --omit=dev --no-audit --no-fund fi if [ "$RESTART" = nein ]; then @@ -142,7 +153,7 @@ jobs: fi echo "--- Dienst neu starten: $DEPLOY_SERVICE" - $SUDO systemctl restart "$DEPLOY_SERVICE" + sudo -n systemctl restart "$DEPLOY_SERVICE" echo "--- Health-Check" for i in $(seq 1 15); do @@ -154,6 +165,6 @@ jobs: done echo "Dienst antwortet nach 30 s nicht. Letzte Zeilen aus dem Journal:" - $SUDO journalctl -u "$DEPLOY_SERVICE" -n 40 --no-pager || true + sudo -n journalctl -u "$DEPLOY_SERVICE" -n 40 --no-pager || true exit 1 REMOTE diff --git a/DEPLOY.md b/DEPLOY.md index 033e233..77bc323 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -487,12 +487,14 @@ Stand selbsttaetig auf den Server aus. Der Workflow liegt in ### Was der Workflow tut -1. Uebertraegt die versionierten Dateien als tar-Paket auf den Server und - gleicht sie dort per `rsync` gegen `/opt/timer-app` ab. (Im Runner-Container - laesst der Docker-Host rsync nicht laufen - `pipe: Permission denied`.) +1. Uebertraegt die versionierten Dateien als tar-Paket und entpackt sie ueber + `/opt/timer-app`. Kein rsync: im Runner-Container verbietet der Docker-Host + dessen `pipe()`-Aufruf, und auf dem Zielhost ist rsync nicht installiert. **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. + sie liegen nicht im Repo und sind damit auch nicht im Paket. + Im Repo geloeschte Dateien werden auf dem Server entfernt: die Liste kommt + aus `git diff` gegen den zuletzt ausgerollten Commit, der als Merkzettel + in `/opt/timer-app/.deploy-commit` liegt. 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.