Zuhause ist künftig git.javagil.de/mi/werkbaum (origin); GitHub bleibt unter dem Remote `github` ein Klon, den scripts/push-github.sh mit `main` versorgt — nur vorwärts, nie erzwungen. Der Klon bleibt aus zwei gemessenen Gründen: Gitea sendet auf raw-Dateien kein Access-Control-Allow-Origin (nachgemessen an mi/werkdock), die ?sourceUrl=-Beispiellinks der READMEs müssen also auf raw.githubusercontent.com zeigen; und der Pages-Workflow lässt sich nicht mitnehmen. Footer, llms.txt und llms.md zeigen jetzt auf Gitea (Repo-Seite ohne Anmeldung lesbar, HTTP 200 geprüft); beide COMMIT_URL-Stellen (deploy-prod.sh und der Pages-Workflow) ziehen mit. deploy-prod.sh prüft origin und github getrennt und erinnert an das Spiegeln, statt selbst zu pushen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
197 lines
8.7 KiB
Bash
Executable File
197 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Werkbaum — Produktions-Deploy via rsync/SSH.
|
|
#
|
|
# Befördert zuerst die fertigen Knoten des mitgelieferten Werkbaum-Plans auf
|
|
# „in Produktion" (scripts/promote-shipped.sh, D30 — genau dieser Deploy macht
|
|
# die Aussage `[^]` wahr), baut dann das badge-freie Prod-Bundle
|
|
# (`npm run build:prod`, ohne den Entwicklungs-Hinweis hinter dem Titel), stellt
|
|
# es lokal genauso zusammen wie der GitHub-Pages-Workflow (LICENSE-Link
|
|
# geradeziehen + Footer-Version/Commit) und spiegelt es per rsync in ein
|
|
# Zielverzeichnis. `--delete`: am Ziel bleibt nichts Altes stehen.
|
|
#
|
|
# Verwendung:
|
|
# scripts/deploy-prod.sh [-y] [--no-promote] [--dry-run] [rsync-ziel]
|
|
#
|
|
# -y ohne Rückfrage befördern und spiegeln (sonst erst Vorschau
|
|
# via --dry-run + Nachfrage)
|
|
# --no-promote Beförderungsschritt überspringen (z. B. Wiederholung eines
|
|
# Deploys, der schon befördert hat)
|
|
# --dry-run nur zeigen, was sich am Ziel änderte, und dann aufhören.
|
|
# Schaltet die Beförderung ausdrücklich mit ab: die macht einen
|
|
# Commit, und ein Probelauf, der etwas schreibt, ist keiner.
|
|
# Gebaut und zusammengestellt wird trotzdem — sonst wüsste der
|
|
# Vergleich nicht, wogegen er läuft.
|
|
#
|
|
# Das Ziel ist entweder das Argument ODER — wenn keins angegeben ist — die
|
|
# Variable DEPLOY_TARGET aus der git-ignorierten Datei .env im Repo-Wurzelordner
|
|
# (Vorlage: .env.example). Ein Argument hat Vorrang.
|
|
#
|
|
# Beispiel (Hostsharing, direkt aufgeschaltete Domain werkbaum.javagil.de —
|
|
# Web-Verzeichnis in `htdocs-ssl/`; als Subdomain unter einer anderen Domain
|
|
# läge es stattdessen in `subs-ssl/<name>/`):
|
|
#
|
|
# scripts/deploy-prod.sh mih00@mih00.hostsharing.net:~/doms/werkbaum.javagil.de/htdocs-ssl
|
|
#
|
|
# ACHTUNG: Das Zielverzeichnis wird als exklusiv für Werkbaum angenommen —
|
|
# `--delete` entfernt dort ALLES, was nicht zum Bundle gehört.
|
|
#
|
|
# Siehe README (Abschnitt Deployment) und docs/DECISIONS.md D16/D19/D30.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---- Argumente ----
|
|
YES=0
|
|
PROMOTE=1
|
|
DRY=0
|
|
TARGET=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-y|--yes) YES=1 ;;
|
|
--no-promote) PROMOTE=0 ;;
|
|
--dry-run) DRY=1; PROMOTE=0 ;;
|
|
-h|--help)
|
|
awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0"
|
|
exit 0 ;;
|
|
-*)
|
|
echo "Unbekannte Option: $arg" >&2; exit 2 ;;
|
|
*)
|
|
if [ -n "$TARGET" ]; then echo "Zu viele Argumente." >&2; exit 2; fi
|
|
TARGET="$arg" ;;
|
|
esac
|
|
done
|
|
|
|
# ---- Repo-Wurzel (Skript ist unter scripts/) ----
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# ---- Ziel: Argument hat Vorrang, sonst DEPLOY_TARGET aus .env (git-ignoriert) ----
|
|
# shellcheck source=/dev/null
|
|
. "$ROOT/scripts/lib-env.sh"
|
|
if [ -z "$TARGET" ]; then
|
|
TARGET="$(env_value DEPLOY_TARGET)"
|
|
[ -n "$TARGET" ] && echo "==> Ziel aus .env: ${TARGET}"
|
|
fi
|
|
|
|
if [ -z "$TARGET" ]; then
|
|
echo "Usage: $0 [-y] <rsync-ziel>" >&2
|
|
echo " oder DEPLOY_TARGET in .env setzen (Vorlage: .env.example)" >&2
|
|
echo " z.B. $0 mih00@mih00.hostsharing.net:~/doms/werkbaum.javagil.de/htdocs-ssl" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# ---- 0) Fertiges auf „in Produktion" befördern (D30) ----
|
|
# MUSS vor dem Build laufen: der Plan ist Build-Eingabe (?raw-Import, D27), und
|
|
# der Commit soll derjenige sein, auf den der Footer-Versionslink zeigt.
|
|
if [ "$PROMOTE" -eq 1 ]; then
|
|
PROMOTE_ARGS=()
|
|
[ "$YES" -eq 1 ] && PROMOTE_ARGS+=(-y)
|
|
"$ROOT/scripts/promote-shipped.sh" "${PROMOTE_ARGS[@]+"${PROMOTE_ARGS[@]}"}"
|
|
else
|
|
echo "==> Beförderung übersprungen (--no-promote)"
|
|
fi
|
|
|
|
# ---- 1) Prod-Build (ohne Build-Hinweis) ----
|
|
if [ ! -d frontend/node_modules ]; then
|
|
echo "==> node_modules fehlt — npm ci"
|
|
npm ci --prefix frontend
|
|
fi
|
|
echo "==> npm run build:prod"
|
|
npm run build:prod --prefix frontend
|
|
|
|
# ---- 2) Staging zusammenstellen (wie der Pages-Workflow, aber lokal) ----
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# Footer-Version: Major.Minor aus VERSION, Micro = Commits seit dem letzten
|
|
# VERSION-Bump; Versionslink zeigt auf den exakt deployten Commit. Best effort —
|
|
# ohne Git bleibt der Quelltext-Platzhalter stehen.
|
|
SED_ARGS=(-e 's#\.\./LICENSE#LICENSE#g')
|
|
if git -C "$ROOT" rev-parse HEAD >/dev/null 2>&1; then
|
|
if [ -n "$(git -C "$ROOT" status --porcelain)" ]; then
|
|
echo " ! Arbeitsbaum ist nicht sauber — der Versions-Commit-Link zeigt auf HEAD," >&2
|
|
echo " der deployte Inhalt kann davon abweichen." >&2
|
|
fi
|
|
MAJORMINOR="$(tr -d '[:space:]' < VERSION)"
|
|
BASE="$(git -C "$ROOT" log -1 --format=%H -- VERSION || true)"
|
|
[ -z "$BASE" ] && BASE="$(git -C "$ROOT" rev-list --max-parents=0 HEAD | tail -1)"
|
|
MICRO="$(git -C "$ROOT" rev-list --count "${BASE}..HEAD")"
|
|
BUILD_VERSION="${MAJORMINOR}.${MICRO}"
|
|
COMMIT_URL="https://git.javagil.de/mi/werkbaum/commit/$(git -C "$ROOT" rev-parse HEAD)"
|
|
echo "==> Footer-Version ${BUILD_VERSION} -> ${COMMIT_URL}"
|
|
# Der Link zeigt auf Gitea (origin, D95) und läuft ins Leere, solange der
|
|
# Commit dort nicht liegt — nach einer Beförderung (Schritt 0) ist das der
|
|
# Normalfall.
|
|
if ! git -C "$ROOT" branch -r --contains HEAD 2>/dev/null | grep -q "^ *origin/"; then
|
|
echo " ! HEAD liegt noch nicht auf origin (Gitea) — der Footer-Versionslink" >&2
|
|
echo " läuft ins Leere, bis 'git push' nachgeholt ist." >&2
|
|
fi
|
|
# GitHub ist seit D95 nur noch ein Klon und wird VON HAND gespiegelt. Der
|
|
# Deploy spiegelt nicht selbst — er erinnert nur, denn die Pages-Instanz und
|
|
# die ?sourceUrl=-Beispiellinks (raw.githubusercontent.com) hängen daran.
|
|
if git -C "$ROOT" remote get-url github >/dev/null 2>&1 &&
|
|
! git -C "$ROOT" branch -r --contains HEAD 2>/dev/null | grep -q "^ *github/"; then
|
|
echo " ! HEAD liegt noch nicht auf github — der Klon hinkt hinterher;" >&2
|
|
echo " 'scripts/push-github.sh' holt es nach." >&2
|
|
fi
|
|
SED_ARGS+=(-e "s#\(<a class=\"ver\" href=\"\)[^\"]*#\1${COMMIT_URL}#")
|
|
SED_ARGS+=(-e "s#\(<a class=\"ver\"[^>]*>\)[0-9.]\+</a>#\1${BUILD_VERSION}</a>#")
|
|
else
|
|
echo " ! kein Git-Repo — Footer behält den Versions-Platzhalter" >&2
|
|
fi
|
|
|
|
sed "${SED_ARGS[@]}" frontend/dist/index.html > "$STAGE/index.html"
|
|
cp LICENSE "$STAGE/LICENSE"
|
|
# Agenten-Fassung der Notation (D43) — liegt per Vite-public/ in dist/
|
|
cp frontend/dist/llms.md "$STAGE/llms.md"
|
|
# Der Wegweiser der llms.txt-Konvention (D43-Nachtrag 2): kurzer Index, der auf
|
|
# llms.md, SPEC und Repo zeigt. Nur er liegt an der Adresse, die Agenten von
|
|
# selbst probieren.
|
|
cp frontend/dist/llms.txt "$STAGE/llms.txt"
|
|
# PWA-Hülle (D73): Manifest + Icons + Service Worker — public/-Assets neben
|
|
# der einen Datei
|
|
cp frontend/dist/manifest.webmanifest "$STAGE/manifest.webmanifest"
|
|
cp frontend/dist/icon-192.png frontend/dist/icon-512.png frontend/dist/icon-maskable-512.png "$STAGE/"
|
|
cp frontend/dist/sw.js "$STAGE/sw.js"
|
|
# Ohne diese Zuordnung liefert Apache `.md` ohne Content-Type aus und der
|
|
# Browser rät windows-1252 (D43-Nachtrag 2). Pages braucht sie nicht.
|
|
#
|
|
# Dieselbe Datei trägt die Proxy-Regel für das Backend (D77). Die Portnummer
|
|
# steht dort als Platzhalter und wird hier eingesetzt — Apache und Dienst
|
|
# müssen sich über genau eine Zahl einig sein, und die steht in `.env`.
|
|
BACKEND_PORT="$(env_value BACKEND_PORT)"; BACKEND_PORT="${BACKEND_PORT:-9080}"
|
|
echo "==> /api/ -> 127.0.0.1:${BACKEND_PORT} (BACKEND_PORT)"
|
|
sed -e "s/__BACKEND_PORT__/${BACKEND_PORT}/g" scripts/prod.htaccess > "$STAGE/.htaccess"
|
|
|
|
# ---- 3) Spiegeln (--delete: nichts Altes bleibt am Ziel) ----
|
|
# --chmod=D755,F644 erzwingt web-taugliche Rechte am Ziel, unabhängig von den
|
|
# lokalen Rechten: `mktemp -d` legt $STAGE mit 0700 an, und `rsync -a` würde
|
|
# diesen Modus sonst auf das Ziel-Verzeichnis übertragen — dann kann der
|
|
# Webserver es nicht betreten (Apache 403 „unable to read htaccess file“).
|
|
# --filter='protect /.well-known/***' bewahrt am Ziel liegende ACME-Challenge-
|
|
# Dateien (Let's-Encrypt-Erneuerung) vor dem --delete, obwohl sie nicht in der
|
|
# Quelle liegen — sonst könnte ein Deploy eine laufende Zertifikatserneuerung
|
|
# abräumen. Bedient Hostsharing die Challenge außerhalb des Docroots, ist es ein
|
|
# No-op.
|
|
RSYNC_OPTS=(-avz --delete --chmod=D755,F644 --filter='protect /.well-known/***')
|
|
if [ "$DRY" -eq 1 ]; then
|
|
echo "==> Probelauf (rsync --dry-run --delete) nach ${TARGET}:"
|
|
rsync "${RSYNC_OPTS[@]}" --dry-run "$STAGE"/ "$TARGET"
|
|
echo "==> Nichts geschrieben (--dry-run)."
|
|
exit 0
|
|
fi
|
|
if [ "$YES" -ne 1 ]; then
|
|
echo "==> Vorschau (rsync --dry-run --delete) nach ${TARGET}:"
|
|
rsync "${RSYNC_OPTS[@]}" --dry-run "$STAGE"/ "$TARGET"
|
|
printf '==> Wirklich spiegeln? --delete löscht am Ziel alles Fremde. [y/N] '
|
|
read -r ANS
|
|
case "$ANS" in
|
|
y|Y|j|J) : ;;
|
|
*) echo "Abgebrochen."; exit 1 ;;
|
|
esac
|
|
fi
|
|
|
|
echo "==> rsync -> ${TARGET}"
|
|
rsync "${RSYNC_OPTS[@]}" "$STAGE"/ "$TARGET"
|
|
echo "==> Fertig."
|