nginx: server_tokens off, HSTS, exaktes /v1 liefert 404 (Regex-Block statt ^~ /v1/). deploy.sh: AIRCARD_WEBHOOK_TOKEN nicht an npm, Sperrverzeichnis gegen parallele Läufe (verwaiste Sperren werden übernommen), timeout 900 für npm ci und Build. Neuer Test deploy/deploy.test.js gegen das echte Skript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
7.6 KiB
Bash
Executable File
187 lines
7.6 KiB
Bash
Executable File
#!/bin/sh
|
|
# AirCard-Deployment (läuft im Container webklar-aircard-build, node:22-alpine, POSIX sh).
|
|
#
|
|
# Ablauf: Repo klonen bzw. aktualisieren -> (bei poll/initial) überspringen, wenn dieser Commit
|
|
# schon versucht wurde -> npm ci -> Build -> web/dist nach $OUT_DIR kopieren.
|
|
#
|
|
# Aufruf: durch webhook-server.js (DEPLOY_TRIGGER=initial|poll|webhook) oder von Hand:
|
|
# docker exec webklar-aircard-build sh /deploy.sh
|
|
#
|
|
# Variablen (Standardwerte in Klammern):
|
|
# REPO_DIR (/app) OUT_DIR (/out) GITEA_URL (https://git.webklar.com) GITEA_USER (knso)
|
|
# GITEA_REPO (AirCardIOS) GITEA_TOKEN (leer = anonym) APP_SUBDIR (web) DEPLOY_BRANCH (main)
|
|
# DEPLOY_TRIGGER (leer = manual)
|
|
#
|
|
# Fehlerzeilen beginnen mit "ERROR:"; der letzte Satz eines erfolgreichen Builds ist
|
|
# "Deployment abgeschlossen". Läuft schon ein Deployment (Sperrverzeichnis in .git), meldet das
|
|
# Skript "läuft bereits" und endet mit 0. npm ci und der Build sind auf 15 Minuten begrenzt (timeout).
|
|
set -e
|
|
|
|
# Jeder Abbruch soll eine ERROR-Zeile hinterlassen, auch wenn set -e ihn auslöst.
|
|
# Die eigene Sperre (LOCK_HELD) wird bei jedem Ende freigegeben, eine fremde nie.
|
|
ERROR_PRINTED=""
|
|
LOCK_HELD=""
|
|
LOCK_DIR=""
|
|
fail() {
|
|
echo "ERROR: $*"
|
|
ERROR_PRINTED=1
|
|
exit 1
|
|
}
|
|
cleanup() {
|
|
status=$?
|
|
if [ "${status}" -ne 0 ] && [ -z "${ERROR_PRINTED}" ]; then
|
|
echo "ERROR: Deployment unerwartet abgebrochen (Exit-Code ${status})"
|
|
fi
|
|
if [ -n "${LOCK_HELD}" ] && [ -n "${LOCK_DIR}" ]; then
|
|
rm -rf "${LOCK_DIR}"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
# Ein Signal (z. B. Zeitlimit des Webhook-Servers) soll ebenfalls über den EXIT-Trap aufräumen.
|
|
trap 'exit 143' HUP INT TERM
|
|
|
|
REPO_DIR="${REPO_DIR:-/app}"
|
|
OUT_DIR="${OUT_DIR:-/out}"
|
|
GITEA_URL="${GITEA_URL:-https://git.webklar.com}"
|
|
GITEA_URL="${GITEA_URL%/}"
|
|
GITEA_USER="${GITEA_USER:-knso}"
|
|
GITEA_REPO="${GITEA_REPO:-AirCardIOS}"
|
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
|
APP_SUBDIR="${APP_SUBDIR:-web}"
|
|
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
|
|
|
|
# Clone-URL: mit Token nur, wenn GITEA_TOKEN gesetzt ist. Die URL mit Token wird nie ausgegeben.
|
|
CLEAN_URL="${GITEA_URL}/${GITEA_USER}/${GITEA_REPO}.git"
|
|
if [ -n "${GITEA_TOKEN}" ]; then
|
|
case "${GITEA_URL}" in
|
|
https://*) REPO_URL="https://${GITEA_TOKEN}@${GITEA_URL#https://}/${GITEA_USER}/${GITEA_REPO}.git" ;;
|
|
http://*) REPO_URL="http://${GITEA_TOKEN}@${GITEA_URL#http://}/${GITEA_USER}/${GITEA_REPO}.git" ;;
|
|
*) fail "GITEA_URL muss mit https:// oder http:// beginnen" ;;
|
|
esac
|
|
else
|
|
REPO_URL="${CLEAN_URL}"
|
|
fi
|
|
|
|
# git-Aufruf, dessen Ausgabe nie eine URL mit Zugangsdaten verrät (git zeigt sie in manchen Fehlermeldungen).
|
|
run_git() {
|
|
_git_log="$(mktemp)"
|
|
if "$@" >"${_git_log}" 2>&1; then
|
|
_git_rc=0
|
|
else
|
|
_git_rc=$?
|
|
fi
|
|
sed 's#//[^/@ ]*@#//***@#g' "${_git_log}"
|
|
rm -f "${_git_log}"
|
|
return "${_git_rc}"
|
|
}
|
|
|
|
echo "=========================================="
|
|
echo "AirCard Deployment (Trigger: ${DEPLOY_TRIGGER:-manual})"
|
|
echo "=========================================="
|
|
echo "Repository: ${GITEA_URL}/${GITEA_USER}/${GITEA_REPO} (Branch ${DEPLOY_BRANCH})"
|
|
echo ""
|
|
|
|
mkdir -p "${REPO_DIR}" "${OUT_DIR}" || fail "Verzeichnisse ${REPO_DIR} und ${OUT_DIR} nicht anlegbar"
|
|
|
|
if [ ! -d "${REPO_DIR}/.git" ]; then
|
|
echo "Klone Repository..."
|
|
# Reste eines abgebrochenen Klons entfernen (inkl. versteckter Dateien); das Verzeichnis selbst ist ein Mount.
|
|
rm -rf "${REPO_DIR}"/* "${REPO_DIR}"/.[!.]* "${REPO_DIR}"/..?*
|
|
run_git git clone --quiet --branch "${DEPLOY_BRANCH}" "${REPO_URL}" "${REPO_DIR}" ||
|
|
fail "Repository konnte nicht geklont werden"
|
|
fi
|
|
|
|
cd "${REPO_DIR}" || fail "Verzeichnis ${REPO_DIR} nicht erreichbar"
|
|
|
|
# Nur ein Deployment gleichzeitig (Webhook, Polling und der manuelle Aufruf teilen sich REPO_DIR und OUT_DIR).
|
|
# mkdir ist atomar. Eine verwaiste Sperre (Prozess weg oder über 60 Minuten alt) wird übernommen.
|
|
LOCK_DIR="${REPO_DIR}/.git/aircard-deploy.lock"
|
|
acquire_lock() {
|
|
mkdir "${LOCK_DIR}" 2>/dev/null || return 1
|
|
echo "$$" >"${LOCK_DIR}/pid"
|
|
LOCK_HELD=1
|
|
}
|
|
if ! acquire_lock; then
|
|
_lock_pid="$(cat "${LOCK_DIR}/pid" 2>/dev/null || true)"
|
|
_lock_stale=""
|
|
if [ -n "${_lock_pid}" ]; then
|
|
kill -0 "${_lock_pid}" 2>/dev/null || _lock_stale=1
|
|
fi
|
|
if [ -n "$(find "${LOCK_DIR}" -maxdepth 0 -mmin +60 2>/dev/null)" ]; then
|
|
_lock_stale=1
|
|
fi
|
|
if [ -n "${_lock_stale}" ]; then
|
|
echo "Verwaiste Sperre gefunden, übernehme sie."
|
|
rm -rf "${LOCK_DIR}"
|
|
acquire_lock || { echo "Deployment läuft bereits, dieser Lauf wird übersprungen."; exit 0; }
|
|
else
|
|
echo "Deployment läuft bereits, dieser Lauf wird übersprungen."
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
echo "Aktualisiere Repository..."
|
|
# Remote-URL bei jedem Lauf neu setzen, damit ein getauschtes Token greift.
|
|
git remote set-url origin "${REPO_URL}" || fail "git remote set-url fehlgeschlagen"
|
|
run_git git fetch --quiet origin "+refs/heads/${DEPLOY_BRANCH}:refs/remotes/origin/${DEPLOY_BRANCH}" ||
|
|
fail "git fetch für Branch ${DEPLOY_BRANCH} fehlgeschlagen"
|
|
# Token nur für clone/fetch: danach aus der Remote-URL entfernen, damit er nicht in .git/config liegen
|
|
# bleibt (dort könnten ihn die Build-Skripte lesen).
|
|
git remote set-url origin "${CLEAN_URL}" || fail "git remote set-url fehlgeschlagen"
|
|
git reset --hard "origin/${DEPLOY_BRANCH}" >/dev/null || fail "git reset auf origin/${DEPLOY_BRANCH} fehlgeschlagen"
|
|
git clean -fd >/dev/null || fail "git clean fehlgeschlagen"
|
|
|
|
CURRENT_COMMIT="$(git rev-parse HEAD)" || fail "git rev-parse HEAD fehlgeschlagen"
|
|
echo "Commit: ${CURRENT_COMMIT}"
|
|
|
|
# Beim Neustart (initial) und beim Polling nur bauen, wenn es einen neuen Commit gibt.
|
|
# Webhook-, manuelle und Aufrufe ohne Trigger bauen immer.
|
|
MARKER="${REPO_DIR}/.git/aircard-last-deployed-commit"
|
|
case "${DEPLOY_TRIGGER:-manual}" in
|
|
poll | initial)
|
|
if [ -f "${MARKER}" ] && [ "$(cat "${MARKER}")" = "${CURRENT_COMMIT}" ] && [ -f "${OUT_DIR}/index.html" ]; then
|
|
echo "Commit ${CURRENT_COMMIT} unverändert, Build übersprungen."
|
|
exit 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Gemerkt wird der zuletzt VERSUCHTE Commit (vor dem Build): ein kaputter Build wird nicht alle
|
|
# 15 Minuten wiederholt, und die alte Seite bleibt online.
|
|
echo "${CURRENT_COMMIT}" >"${MARKER}" || fail "Marker ${MARKER} nicht schreibbar"
|
|
|
|
[ -d "${REPO_DIR}/${APP_SUBDIR}" ] || fail "Ordner ${APP_SUBDIR} fehlt im Repository"
|
|
cd "${REPO_DIR}/${APP_SUBDIR}"
|
|
|
|
# Zugangsdaten nicht an npm und die Build-Skripte weiterreichen (auch das Webhook-Geheimnis steht im
|
|
# Container und käme sonst beim manuellen Aufruf `docker exec ... sh /deploy.sh` bei npm an).
|
|
unset GITEA_TOKEN REPO_URL AIRCARD_WEBHOOK_TOKEN
|
|
|
|
# Ein hängender Build soll nicht ewig laufen (und alle späteren Deployments sperren): 15 Minuten.
|
|
# `timeout` gibt es in busybox (node:22-alpine); fehlt es, läuft der Build ohne Limit.
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
LIMIT="timeout 900"
|
|
else
|
|
LIMIT=""
|
|
fi
|
|
|
|
echo ""
|
|
echo "Installiere Abhängigkeiten..."
|
|
# shellcheck disable=SC2086 # LIMIT besteht absichtlich aus zwei Wörtern
|
|
${LIMIT} npm ci --no-audit --no-fund || fail "npm ci fehlgeschlagen (oder das Zeitlimit von 15 Minuten ist erreicht)"
|
|
|
|
echo ""
|
|
echo "Baue die Seite..."
|
|
# VITE_*-Variablen kommen direkt aus der Container-Umgebung (compose).
|
|
rm -rf dist
|
|
# shellcheck disable=SC2086
|
|
${LIMIT} npm run build || fail "Build fehlgeschlagen (oder das Zeitlimit von 15 Minuten ist erreicht)"
|
|
[ -f dist/index.html ] || fail "dist/index.html fehlt nach dem Build"
|
|
|
|
echo ""
|
|
echo "Kopiere nach ${OUT_DIR}..."
|
|
rm -rf "${OUT_DIR}"/* "${OUT_DIR}"/.[!.]* "${OUT_DIR}"/..?*
|
|
cp -r dist/. "${OUT_DIR}/" || fail "Kopieren nach ${OUT_DIR} fehlgeschlagen"
|
|
|
|
echo "Deployment abgeschlossen"
|