#!/usr/bin/env bash # # paas-self-deploy — update the PLATFORM itself (paas CLI + panel) on this # server from a repo tarball, re-running bootstrap.sh with the settings the # instance already has. This is what the Jenkins job `paas-platform` # (ci/Jenkinsfile.platform) calls over SSH; it is also usable by hand. # # tar czf - -C . | sudo -n paas-self-deploy deploy [] # sudo paas-self-deploy verify # health summary, exit 1 if unhealthy # sudo paas-self-deploy rollback # swap back to the previous checkout + bootstrap # sudo paas-self-deploy status # what is deployed, last log lines # # Runs as root (installed to /usr/local/sbin by bootstrap.sh; the deploy user # may run it via the exact-command sudoers line in /etc/sudoers.d/paas-ci). # Nothing here is instance-specific: the panel domain, flavour (Node-only or # Node+Laravel), PHP versions, system-domain base and ACME e-mail are read # back from the running instance, so a Laravel server stays Laravel and a # server with its own panel domain keeps it. Apps, paas.db and the panel # .env are never touched — bootstrap.sh is idempotent. # # Steps for `deploy`: extract to a temp dir → lint (bash -n, node --check) → # copy the current checkout to .prev → rsync the new tree in → # bootstrap.sh → verify. Lint failures leave the server untouched; a failed # bootstrap/verify leaves the new tree in place (so it can be inspected) and # `rollback` restores the previous one. set -euo pipefail [[ "$(id -u)" -eq 0 ]] || { echo "run as root (sudo)" >&2; exit 1; } LOG=/var/log/paas-self-deploy.log LOCK=/run/lock/paas-self-deploy.lock PANEL_ENV=/srv/paas/panel/.env PAAS_CONF=/srv/paas/paas.conf log() { printf '[self-deploy] %s\n' "$*" | tee -a "$LOG"; } die() { log "ERROR: $*"; exit 1; } # ---- locate the checkout the same way paas-init does ------------------- CHECKOUT="${PAAS_CHECKOUT:-}" if [[ -z "$CHECKOUT" ]]; then for d in /home/younes/dotnews-paas /home/*/dotnews-paas /root/dotnews-paas /opt/dotnews-paas; do [[ -f "$d/bootstrap.sh" && -f "$d/bin/paas" ]] && { CHECKOUT="$d"; break; } done fi [[ -n "$CHECKOUT" ]] || CHECKOUT=/home/younes/dotnews-paas PREV="$CHECKOUT.prev" OWNER="$(stat -c %U "$CHECKOUT" 2>/dev/null || echo younes)" id "$OWNER" >/dev/null 2>&1 || OWNER=root # ---- instance settings bootstrap.sh must be re-run with ----------------- instance_env() { local panel base email runtimes php panel="$(sed -n 's#^PANEL_BASE_URL=https\?://##p' "$PANEL_ENV" 2>/dev/null | head -1)" base="$(sed -n 's/^SYSTEM_DOMAIN_BASE=//p' "$PAAS_CONF" 2>/dev/null | head -1)" email="$(sed -n 's/^[[:space:]]*email[[:space:]]\+\([^[:space:]]*\).*/\1/p' /etc/caddy/Caddyfile 2>/dev/null | head -1)" runtimes="$(sed -n 's/^RUNTIMES=//p' "$PAAS_CONF" 2>/dev/null | head -1)" php="$(sed -n 's/^PHP_VERSIONS=//p' "$PAAS_CONF" 2>/dev/null | head -1 | tr -d '"')" ENV=() [[ -n "$panel" ]] && ENV+=("PANEL_DOMAIN=$panel") [[ -n "$base" ]] && ENV+=("SYSTEM_DOMAIN_BASE=$base") [[ -n "$email" ]] && ENV+=("ACME_EMAIL=$email") if [[ "$runtimes" == *laravel* ]]; then ENV+=(WITH_LARAVEL=1) [[ -n "$php" ]] && ENV+=("PHP_VERSIONS=$php") fi # never prompt for an admin password on a re-run (the .env already exists; # bootstrap only asks when it is missing) ENV+=("PANEL_ADMIN_PASSWORD=${PANEL_ADMIN_PASSWORD:-unused}") } run_bootstrap() { instance_env log "bootstrap.sh (${ENV[*]/PANEL_ADMIN_PASSWORD=*/})" if ! ( cd "$CHECKOUT" && env "${ENV[@]}" ./bootstrap.sh ) >>"$LOG" 2>&1; then tail -20 "$LOG" die "bootstrap.sh failed — full log: $LOG" fi # keep this script itself current (bootstrap installs it too, belt and braces) [[ -f "$CHECKOUT/infra/paas-self-deploy" ]] && install -m 0755 "$CHECKOUT/infra/paas-self-deploy" /usr/local/sbin/paas-self-deploy [[ -f "$CHECKOUT/infra/paas-ci-ssh" ]] && install -m 0755 "$CHECKOUT/infra/paas-ci-ssh" /usr/local/bin/paas-ci-ssh return 0 } lint_tree() { # $1 dir local d="$1" f ok=1 [[ -f "$d/bootstrap.sh" && -f "$d/bin/paas" && -f "$d/panel/server.js" ]] \ || die "tarball does not look like a dotnews-paas checkout (bootstrap.sh, bin/paas, panel/server.js)" for f in "$d"/bootstrap.sh "$d"/bin/* "$d"/infra/*.sh "$d"/infra/paas-self-deploy "$d"/infra/paas-ci-ssh; do [[ -f "$f" ]] || continue head -1 "$f" | grep -q 'bash' || continue bash -n "$f" || { log "bash -n failed: ${f#"$d"/}"; ok=0; } done if command -v node >/dev/null; then for f in "$d"/panel/*.js "$d"/panel/lib/*.js "$d"/panel/bin/*.js; do [[ -f "$f" ]] || continue node --check "$f" 2>>"$LOG" || { log "node --check failed: ${f#"$d"/}"; ok=0; } done fi [[ "$ok" -eq 1 ]] || die "lint failed — nothing was changed on this server" log "lint ok" } verify() { local port fails=0 code port="$(sed -n 's/^PANEL_PORT=//p' "$PANEL_ENV" 2>/dev/null | head -1)"; port="${port:-3900}" for s in caddy varnish; do if systemctl is-enabled "$s" >/dev/null 2>&1; then systemctl is-active --quiet "$s" && log "$s: active" || { log "$s: NOT active"; fails=$((fails+1)); } fi done local pm2 pm2="$(sudo -u deploy -H pm2 jlist 2>/dev/null | node -e ' let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);const p=j.find(x=>x.name==="paas-panel");console.log(p?p.pm2_env.status:"absent")}catch{console.log("unknown")}})' 2>/dev/null || echo unknown)" [[ "$pm2" == "online" ]] && log "pm2 paas-panel: online" || { log "pm2 paas-panel: $pm2"; fails=$((fails+1)); } code=000 for _ in $(seq 1 15); do code="$(curl -s -o /dev/null -m 5 -w '%{http_code}' "http://127.0.0.1:$port/login" || echo 000)" [[ "$code" == "200" ]] && break sleep 2 done [[ "$code" == "200" ]] && log "panel /login: 200" || { log "panel /login: $code"; fails=$((fails+1)); } if [[ -f /srv/paas/panel/server.js && -f "$CHECKOUT/panel/server.js" ]]; then cmp -s /srv/paas/panel/server.js "$CHECKOUT/panel/server.js" \ && log "installed panel matches checkout" || { log "installed panel DIFFERS from checkout"; fails=$((fails+1)); } fi [[ -f "$CHECKOUT/.deployed" ]] && log "deployed: $(cat "$CHECKOUT/.deployed")" [[ "$fails" -eq 0 ]] || die "$fails check(s) failed — 'paas-self-deploy rollback' restores the previous checkout" log "verify ok" } cmd="${1:-}"; shift || true case "$cmd" in deploy) commit="${1:-unknown}"; msg="${2:-}" [[ "$commit" =~ ^[A-Za-z0-9._/-]{1,64}$ ]] || die "bad commit id" exec 9>"$LOCK"; flock -n 9 || die "another self-deploy is running" [[ ! -t 0 ]] || die "expected a .tar.gz of the repo on stdin" tmp="$(mktemp -d /tmp/paas-self-deploy.XXXXXX)" trap 'rm -rf "$tmp"' EXIT log "===== deploy $commit ${msg:+— $msg }(by ${SUDO_USER:-root}) =====" tar xzf - -C "$tmp" --warning=no-unknown-keyword || die "could not extract tarball" lint_tree "$tmp" if [[ -d "$CHECKOUT" ]]; then rsync -a --delete "$CHECKOUT/" "$PREV/" log "previous checkout saved to $PREV" fi install -d -o "$OWNER" -g "$OWNER" "$CHECKOUT" rsync -a --delete --exclude .git --exclude .idea --exclude .claude --exclude node_modules \ --exclude .env --exclude '*.db' "$tmp/" "$CHECKOUT/" printf '%s %s %s\n' "$commit" "$(date -Is)" "$msg" >"$CHECKOUT/.deployed" chown -R "$OWNER:$OWNER" "$CHECKOUT" log "checkout updated ($CHECKOUT @ $commit)" run_bootstrap verify log "===== deploy $commit done =====" ;; rollback) exec 9>"$LOCK"; flock -n 9 || die "another self-deploy is running" [[ -d "$PREV" && -f "$PREV/bootstrap.sh" ]] || die "no previous checkout at $PREV" log "===== rollback to $(cat "$PREV/.deployed" 2>/dev/null || echo 'previous checkout') =====" tmp="$CHECKOUT.swap.$$" mv "$CHECKOUT" "$tmp" && mv "$PREV" "$CHECKOUT" && mv "$tmp" "$PREV" run_bootstrap verify log "===== rollback done =====" ;; verify) verify ;; status) echo "checkout: $CHECKOUT" echo "deployed: $(cat "$CHECKOUT/.deployed" 2>/dev/null || echo '(unknown — not deployed via self-deploy yet)')" [[ -d "$PREV" ]] && echo "previous: $(cat "$PREV/.deployed" 2>/dev/null || echo '(present)')" echo "--- last log lines ($LOG)" tail -15 "$LOG" 2>/dev/null || true ;; *) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' exit 2 ;; esac