Abgleich ohne rsync ueber einen Commit-Merkzettel
Deploy auf den Timer-Server / deploy (push) Successful in 18s

Auf dem Zielhost gibt es kein rsync (und im Runner-Container laeuft es
wegen des Docker-Hosts ohnehin nicht). Der ausgerollte Commit steht jetzt
in /opt/timer-app/.deploy-commit; die Aenderungsliste kommt aus git diff
gegen diesen Stand.

Das ist genauer als ein Zeitstempelvergleich: geloeschte Dateien sind als
solche benannt und werden gezielt entfernt, und ob npm ci und ein Neustart
noetig sind, entscheidet dieselbe Liste. Ist kein Vorgaengerstand bekannt,
gilt alles als neu.

Der Pruefschritt bricht jetzt ab, wenn der Deploy-Benutzer im Zielpfad
nicht schreiben darf - das ist besser als ein halb ausgerollter Stand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 18:10:07 +02:00
co-authored by Claude Opus 5
parent b85e4610e0
commit 5e83999950
2 changed files with 59 additions and 46 deletions
+52 -41
View File
@@ -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
+7 -5
View File
@@ -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.