#!/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"