From 61b7b8ef20c441fa5530a7b6a01d2143373de64e Mon Sep 17 00:00:00 2001 From: m17hr1l Date: Mon, 25 May 2026 14:51:47 +0200 Subject: [PATCH] =?UTF-8?q?stage-28:=20deploy.sh=20=E2=80=94=20idempotent?= =?UTF-8?q?=20remote=20deploy=20+=20health=20probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/deploy.sh pushes the current branch to origin, ssh's into the prod box (neuronetz@cloud.neuronetz.ai:/home/neuronetz/docker-public/ neuro-psyc by default — overridable via env vars), clones-or-pulls, ensures the external 'backend' docker network exists, runs docker compose up -d --build (+ --profile gpu if PSYC_PROD_GPU=1), and then verifies the cockpit is healthy both on prod-internal :8767 and at the public URL — so the script ends knowing whether the page is up. Refuses to touch prod's .env (warns + copies .env.example if missing, so you can edit it manually). Never transfers data/ or adapters (gitignored; prod fetches its own corpus). Color output, idempotent, safe to re-run. Co-Authored-By: Claude Opus 4.7 --- scripts/deploy.sh | 147 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100755 scripts/deploy.sh diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..7b42bf3 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# deploy.sh — sync this branch to the prod box and verify the cockpit is serving. +# +# Usage: scripts/deploy.sh +# +# Env vars (all have defaults — override only if your setup differs): +# PSYC_PROD_HOST default: neuronetz@cloud.neuronetz.ai +# PSYC_PROD_PATH default: /home/neuronetz/docker-public/neuro-psyc +# PSYC_PROD_URL default: https://psyc.neuronetz.ai +# PSYC_PROD_GPU set to 1 to also bring up the inference (GPU) service +# PSYC_GIT_REMOTE default: origin +# PSYC_BRANCH default: the currently checked-out branch +# +# What it does (idempotent — safe to re-run): +# 1. push the current branch to origin +# 2. ssh into prod, clone the repo if missing, pull the branch +# 3. docker compose up -d --build (+ gpu profile if PSYC_PROD_GPU=1) +# 4. probe :8767/healthz on the prod box + the public URL; report state +# +# What it does NOT do: +# • touch .env on the prod box (set keys there once, manually — gitignored) +# • transfer data/ or model artifacts (gitignored; prod fetches its own) +# • configure DNS or TLS (that's the reverse-proxy + acme-companion side) + +set -euo pipefail + +HOST="${PSYC_PROD_HOST:-neuronetz@cloud.neuronetz.ai}" +REMOTE_PATH="${PSYC_PROD_PATH:-/home/neuronetz/docker-public/neuro-psyc}" +PUBLIC_URL="${PSYC_PROD_URL:-https://psyc.neuronetz.ai}" +GIT_REMOTE="${PSYC_GIT_REMOTE:-origin}" +BRANCH="${PSYC_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" +WITH_GPU="${PSYC_PROD_GPU:-}" + +# ── tty styling ───────────────────────────────────────────────────────── +if [[ -t 1 ]]; then + B=$'\e[1m'; D=$'\e[2m'; R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; C=$'\e[36m'; Z=$'\e[0m' +else B=; D=; R=; G=; Y=; C=; Z=; fi +say() { printf "%s[deploy]%s %s\n" "$C" "$Z" "$*"; } +ok() { printf "%s[deploy]%s %s%s%s\n" "$G" "$Z" "$G" "$*" "$Z"; } +warn() { printf "%s[deploy]%s %s%s%s\n" "$Y" "$Z" "$Y" "$*" "$Z"; } +fail() { printf "%s[deploy]%s %s%s%s\n" "$R" "$Z" "$R" "$*" "$Z" >&2; exit 1; } + +# ── 0. preflight ──────────────────────────────────────────────────────── +command -v ssh >/dev/null || fail "ssh not installed locally" +command -v git >/dev/null || fail "git not installed locally" +command -v curl >/dev/null || fail "curl not installed locally" +[[ -d .git ]] || fail "run from the psyc repo root (no .git here)" + +if ! git diff --quiet HEAD -- 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + warn "local working tree has uncommitted changes — they won't be deployed (git push only sends commits)." +fi + +GIT_URL=$(git config --get "remote.${GIT_REMOTE}.url") \ + || fail "no remote '${GIT_REMOTE}' configured locally" + +# ── 1. local push ─────────────────────────────────────────────────────── +say "pushing ${B}${BRANCH}${Z} to ${B}${GIT_REMOTE}${Z}…" +git push "${GIT_REMOTE}" "${BRANCH}" || fail "git push failed — fix and retry" +LOCAL_REV=$(git rev-parse --short HEAD) +ok "pushed ${BRANCH} @ ${LOCAL_REV}" + +# ── 2. remote bring-up ────────────────────────────────────────────────── +say "deploying to ${B}${HOST}:${REMOTE_PATH}${Z}…" + +COMPOSE_PROFILES="" +[[ -n "$WITH_GPU" ]] && COMPOSE_PROFILES="--profile gpu" + +# heredoc runs on the prod box. Local vars are interpolated by THIS shell; +# remote vars start with \$ so they're set on the remote side. +ssh -o StrictHostKeyChecking=accept-new -T "${HOST}" bash -s </dev/null || git checkout --quiet -b "\$BRANCH" "origin/\$BRANCH" +prn "git pull --ff-only origin \$BRANCH" +git pull --quiet --ff-only origin "\$BRANCH" +REMOTE_REV=\$(git rev-parse --short HEAD) +prn "now at \$REMOTE_REV" + +# 2c. .env sanity +if [[ ! -f .env ]]; then + prn "WARNING: .env missing — copying .env.example. Edit it before psyc fetch-all will work." + cp .env.example .env +fi + +# 2d. external 'backend' network for nginx-proxy +if ! docker network ls --format '{{.Name}}' | grep -qx backend; then + prn "creating external docker network 'backend'" + docker network create backend +fi + +# 2e. compose up +prn "docker compose up -d --build \$COMPOSE_PROFILES" +docker compose up -d --build \$COMPOSE_PROFILES + +prn "container status:" +docker compose ps --format "table {{.Name}}\t{{.Status}}" | sed 's/^/ /' +REMOTE + +ok "remote bring-up complete" + +# ── 3. internal health probe (on the prod box localhost) ─────────────── +say "probing ${B}127.0.0.1:8767/healthz${Z} on prod (up to 90s)…" +REMOTE_HEALTH=$(ssh -o StrictHostKeyChecking=accept-new "${HOST}" ' + for i in $(seq 1 45); do + if curl -fs http://127.0.0.1:8767/healthz >/dev/null 2>&1; then echo OK; exit 0; fi + sleep 2 + done + echo TIMEOUT') + +if [[ "${REMOTE_HEALTH}" != *OK* ]]; then + fail "cockpit unhealthy on prod after 90s — ssh ${HOST}, cd ${REMOTE_PATH}, run 'docker compose logs cockpit' to debug" +fi +ok "cockpit healthy on prod" + +# ── 4. external probe via the public URL ──────────────────────────────── +say "probing ${B}${PUBLIC_URL}/healthz${Z} from here…" +if curl --max-time 8 -fs "${PUBLIC_URL}/healthz" >/dev/null 2>&1; then + INF=$(curl --max-time 5 -s "${PUBLIC_URL}/api/inference-status" || printf '%s' '{}') + ok "${PUBLIC_URL} is LIVE" + printf " inference: %s\n" "${INF}" +else + warn "public URL not reachable from here — most likely DNS or TLS isn't finished" + warn " diag:" + warn " dig +short psyc.neuronetz.ai → expect A record to prod IP" + warn " on the prod-host: docker logs acme-companion --tail 30" + warn " cockpit IS healthy on prod-internal :8767 — the app is fine, the front isn't there yet" +fi + +ok "done — deployed ${BRANCH} @ ${LOCAL_REV}"