Abgleich ohne rsync ueber einen Commit-Merkzettel
Deploy auf den Timer-Server / deploy (push) Successful in 18s
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:
@@ -1,7 +1,7 @@
|
|||||||
name: Deploy auf den Timer-Server
|
name: Deploy auf den Timer-Server
|
||||||
|
|
||||||
# Laeuft bei jedem Push auf main und laesst sich in der Gitea-Oberflaeche
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -10,32 +10,28 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
# Nicht ubuntu-latest (24.04): in dem Image scheitern auf diesem Runner
|
# Nicht ubuntu-latest (24.04): in dem Image scheitern auf diesem Runner
|
||||||
# moderne Syscalls - actions/checkout stirbt mit "read ENOTCONN", rsync
|
# moderne Syscalls - actions/checkout stirbt mit "read ENOTCONN".
|
||||||
# mit "pipe: Permission denied". Ursache ist das seccomp-Profil des
|
# Ursache ist das seccomp-Profil des Docker-Hosts, nicht der Workflow.
|
||||||
# Docker-Hosts, nicht der Workflow. Das aeltere Image kommt damit klar.
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Repository-Variablen (Settings -> Actions -> Variables)
|
|
||||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||||
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
|
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
|
||||||
DEPLOY_SERVICE: ${{ vars.DEPLOY_SERVICE }}
|
DEPLOY_SERVICE: ${{ vars.DEPLOY_SERVICE }}
|
||||||
HEALTH_URL: ${{ vars.HEALTH_URL }}
|
HEALTH_URL: ${{ vars.HEALTH_URL }}
|
||||||
# Repository-Secrets (Settings -> Actions -> Secrets)
|
|
||||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Bewusst kein actions/checkout: die JS-Aktion stirbt im Runner-Image
|
# 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
|
# beim Aufruf von git mit "read ENOTCONN". Ein Klon per Shell umgeht das.
|
||||||
# im Container). Ein Klon per Shell umgeht das und braucht kein Node.
|
|
||||||
- name: Werkzeuge und Code holen
|
- name: Werkzeuge und Code holen
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
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
|
rm -rf quelle && mkdir quelle && cd quelle
|
||||||
git init -q
|
git init -q
|
||||||
@@ -43,7 +39,6 @@ jobs:
|
|||||||
# deshalb Schema und Rest getrennt behandeln statt https anzunehmen.
|
# deshalb Schema und Rest getrennt behandeln statt https anzunehmen.
|
||||||
URL="${GITHUB_SERVER_URL%/}"
|
URL="${GITHUB_SERVER_URL%/}"
|
||||||
git remote add origin "${URL%%://*}://x-access-token:$TOKEN@${URL#*://}/$GITHUB_REPOSITORY.git"
|
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 fetch -q --depth 1 origin "$GITHUB_SHA" || git fetch -q --depth 1 origin "$GITHUB_REF_NAME"
|
||||||
git checkout -q FETCH_HEAD
|
git checkout -q FETCH_HEAD
|
||||||
echo "Ausgecheckt: $(git log --oneline -1)"
|
echo "Ausgecheckt: $(git log --oneline -1)"
|
||||||
@@ -69,47 +64,65 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
chmod 644 ~/.ssh/known_hosts
|
chmod 644 ~/.ssh/known_hosts
|
||||||
|
|
||||||
- name: Verbindung pruefen
|
- name: Zielhost pruefen
|
||||||
run: |
|
run: |
|
||||||
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" "DEPLOY_PATH='$DEPLOY_PATH' bash -s" <<'PRUEF'
|
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" "DEPLOY_PATH='$DEPLOY_PATH' bash -s" <<'PRUEF'
|
||||||
echo "Angemeldet als $(id -un) auf $(hostname)"
|
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)"
|
printf ' %-10s %s\n' "$w" "$(command -v $w || echo FEHLT)"
|
||||||
done
|
done
|
||||||
echo "Rechte:"
|
echo "Rechte:"
|
||||||
ls -ld "$DEPLOY_PATH" "$DEPLOY_PATH/backend" "$DEPLOY_PATH/data" 2>&1 | sed 's/^/ /'
|
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
|
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
|
else
|
||||||
echo " FEHLGESCHLAGEN - der Deploy-Benutzer darf dort nicht schreiben"
|
echo "Schreibtest: FEHLGESCHLAGEN - $(id -un) darf in $DEPLOY_PATH nicht schreiben"; exit 1
|
||||||
fi
|
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
|
PRUEF
|
||||||
|
|
||||||
- name: Dateien uebertragen und abgleichen
|
- name: Aenderungen gegen den ausgerollten Stand ermitteln
|
||||||
run: |
|
run: |
|
||||||
# rsync laeuft NICHT im Runner-Container: dessen Docker-Host verbietet
|
# Der zuletzt ausgerollte Commit liegt als Merkzettel auf dem Server.
|
||||||
# den pipe()-Aufruf ("pipe: Permission denied"). Deshalb geht das
|
# Daraus wird die Dateiliste bestimmt - genauer als ein Zeitstempel-
|
||||||
# Paket per tar hinueber und der Abgleich passiert auf dem Zielhost.
|
# 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 .
|
tar -czf paket.tgz -C quelle --exclude=.git --exclude=.gitea .
|
||||||
|
|
||||||
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
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' \
|
"tar -xzf - -C '$DEPLOY_PATH'" < paket.tgz
|
||||||
< paket.tgz
|
echo "Paket entpackt."
|
||||||
|
|
||||||
# --delete raeumt geloeschte Dateien weg. Die ausgeschlossenen Pfade
|
# Im Repo geloeschte Dateien auch auf dem Server entfernen.
|
||||||
# sind davon ausgenommen (kein --delete-excluded!): Konfiguration,
|
GELOESCHT=$(awk '$1 ~ /^D/ {print $2}' aenderungen.txt)
|
||||||
# Datenbank und node_modules bleiben unangetastet.
|
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" \
|
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
"DEPLOY_PATH='$DEPLOY_PATH' bash -euo pipefail -s" > uebertragen.txt <<'SYNC'
|
"echo '$GITHUB_SHA' > '$DEPLOY_PATH/.deploy-commit'"
|
||||||
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
|
|
||||||
|
|
||||||
- name: Abhaengigkeiten, Neustart und Health-Check
|
- name: Abhaengigkeiten, Neustart und Health-Check
|
||||||
run: |
|
run: |
|
||||||
@@ -118,9 +131,9 @@ jobs:
|
|||||||
# den Zielhost und nicht in den Runner-Container.
|
# den Zielhost und nicht in den Runner-Container.
|
||||||
NPM=nein
|
NPM=nein
|
||||||
RESTART=nein
|
RESTART=nein
|
||||||
if grep -q 'backend/package' uebertragen.txt; then NPM=ja; fi
|
if awk '{print $2}' aenderungen.txt | grep -q '^backend/package'; then NPM=ja; fi
|
||||||
if grep -qE '^(backend/|deploy/)' uebertragen.txt; then RESTART=ja; fi
|
if awk '{print $2}' aenderungen.txt | grep -qE '^(backend/|deploy/)'; then RESTART=ja; fi
|
||||||
if [ ! -s uebertragen.txt ]; then echo "Nichts geaendert."; fi
|
if [ ! -s aenderungen.txt ]; then echo "Nichts geaendert."; fi
|
||||||
echo "npm ci: $NPM | Neustart: $RESTART"
|
echo "npm ci: $NPM | Neustart: $RESTART"
|
||||||
|
|
||||||
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
ssh -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
@@ -128,12 +141,10 @@ jobs:
|
|||||||
DEPLOY_PATH='$DEPLOY_PATH' DEPLOY_SERVICE='$DEPLOY_SERVICE' \
|
DEPLOY_PATH='$DEPLOY_PATH' DEPLOY_SERVICE='$DEPLOY_SERVICE' \
|
||||||
HEALTH_URL='${HEALTH_URL:-http://127.0.0.1:3003/health}' \
|
HEALTH_URL='${HEALTH_URL:-http://127.0.0.1:3003/health}' \
|
||||||
bash -euo pipefail -s" <<'REMOTE'
|
bash -euo pipefail -s" <<'REMOTE'
|
||||||
SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"
|
|
||||||
|
|
||||||
if [ "$NPM" = ja ]; then
|
if [ "$NPM" = ja ]; then
|
||||||
echo "--- npm ci (nur Produktionsabhaengigkeiten)"
|
echo "--- npm ci (nur Produktionsabhaengigkeiten)"
|
||||||
cd "$DEPLOY_PATH/backend"
|
cd "$DEPLOY_PATH/backend"
|
||||||
$SUDO npm ci --omit=dev --no-audit --no-fund
|
npm ci --omit=dev --no-audit --no-fund
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$RESTART" = nein ]; then
|
if [ "$RESTART" = nein ]; then
|
||||||
@@ -142,7 +153,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "--- Dienst neu starten: $DEPLOY_SERVICE"
|
echo "--- Dienst neu starten: $DEPLOY_SERVICE"
|
||||||
$SUDO systemctl restart "$DEPLOY_SERVICE"
|
sudo -n systemctl restart "$DEPLOY_SERVICE"
|
||||||
|
|
||||||
echo "--- Health-Check"
|
echo "--- Health-Check"
|
||||||
for i in $(seq 1 15); do
|
for i in $(seq 1 15); do
|
||||||
@@ -154,6 +165,6 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "Dienst antwortet nach 30 s nicht. Letzte Zeilen aus dem Journal:"
|
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
|
exit 1
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|||||||
@@ -487,12 +487,14 @@ Stand selbsttaetig auf den Server aus. Der Workflow liegt in
|
|||||||
|
|
||||||
### Was der Workflow tut
|
### Was der Workflow tut
|
||||||
|
|
||||||
1. Uebertraegt die versionierten Dateien als tar-Paket auf den Server und
|
1. Uebertraegt die versionierten Dateien als tar-Paket und entpackt sie ueber
|
||||||
gleicht sie dort per `rsync` gegen `/opt/timer-app` ab. (Im Runner-Container
|
`/opt/timer-app`. Kein rsync: im Runner-Container verbietet der Docker-Host
|
||||||
laesst der Docker-Host rsync nicht laufen - `pipe: Permission denied`.)
|
dessen `pipe()`-Aufruf, und auf dem Zielhost ist rsync nicht installiert.
|
||||||
**Nicht angefasst** werden `backend/.env`, `data/` und `node_modules/` —
|
**Nicht angefasst** werden `backend/.env`, `data/` und `node_modules/` —
|
||||||
sie sind vom Abgleich ausgenommen und ueberleben auch das `--delete`,
|
sie liegen nicht im Repo und sind damit auch nicht im Paket.
|
||||||
das geloeschte Dateien auf dem Server aufraeumt.
|
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.
|
2. `npm ci --omit=dev` **nur**, wenn sich `backend/package*.json` geaendert hat.
|
||||||
Das laeuft bewusst auf dem Zielhost: `bcrypt` und `sqlite3` sind native
|
Das laeuft bewusst auf dem Zielhost: `bcrypt` und `sqlite3` sind native
|
||||||
Module und muessen zur dortigen Architektur passen.
|
Module und muessen zur dortigen Architektur passen.
|
||||||
|
|||||||
Reference in New Issue
Block a user