#!/bin/sh set -eu DEPLOY_PATH="${DEPLOY_PATH:-/home/projekte_bao/nexus}" ENV_TMPFILE_TEMPLATE="${ENV_TMPFILE:-/tmp/nexus-deploy-env}" ENV_TMPFILE="" BASE_URL="${BASE_URL:-https://nexus.noveria.net}" BOOTSTRAP_OWNER_EMAIL="${BOOTSTRAP_OWNER_EMAIL_DEPLOY:-vmbao62@hotmail.de}" cleanup() { if [ -n "$ENV_TMPFILE" ] && [ -f "$ENV_TMPFILE" ]; then shred -u "$ENV_TMPFILE" 2>/dev/null || rm -f "$ENV_TMPFILE" fi } trap cleanup EXIT INT TERM require_env() { name="$1" eval "value=\${$name:-}" if [ -z "$value" ]; then echo "Missing required environment variable: $name" >&2 exit 1 fi } require_env ENV_POSTGRES_PASSWORD require_env ENV_JWT_KEY secure_tmpfile() { template="$1" dir="$(dirname "$template")" base="$(basename "$template")" mkdir -p "$dir" mktemp "$dir/$base.XXXXXX" } ENV_TMPFILE="$(secure_tmpfile "$ENV_TMPFILE_TEMPLATE")" chmod 600 "$ENV_TMPFILE" if [ ! -f VERSION ]; then echo "VERSION file not found" >&2 exit 1 fi VERSION="$(tr -d '[:space:]' < VERSION)" if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "Invalid VERSION value: $VERSION" >&2 exit 1 fi GIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo unknown)" GIT_REF="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" echo "Deploying Nexus v$VERSION from $GIT_REF" umask 077 cat > "$ENV_TMPFILE" </dev/null || true echo "Sanitized agents config written to $AGENTS_SANITIZED_PATH" else echo "WARNING: openclaw.json not found at $OPENCLAW_CONFIG — agents-sanitized.json NOT generated" >&2 fi echo "Building and starting Docker compose stack" docker run --rm \ -v "$DEPLOY_PATH:/workspace/nexus" \ -v /var/run/docker.sock:/var/run/docker.sock \ -w /workspace/nexus \ -i \ docker:cli \ sh -c 'set -eu umask 077 cat > /tmp/nexus-deploy-env trap '\''rm -f /tmp/nexus-deploy-env'\'' EXIT INT TERM docker compose --env-file /tmp/nexus-deploy-env build # ── Postgres: only recreate if image or config changed ── # docker compose up -d (without --force-recreate) is smart enough # to only recreate containers whose config or image has changed. # We DROP --force-recreate so postgres persists across deploys # unless its image tag or compose config actually changed. docker compose --env-file /tmp/nexus-deploy-env up -d --remove-orphans --wait docker compose --env-file /tmp/nexus-deploy-env ps ' < "$ENV_TMPFILE" echo "Verifying image provenance" for container in nexus-api-1 nexus-web-1; do revision="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$container")" version="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$container")" if [ "$revision" != "$GIT_SHA" ]; then echo "Image revision mismatch for $container: expected $GIT_SHA, got $revision" >&2 exit 1 fi if [ "$version" != "$VERSION" ]; then echo "Image version mismatch for $container: expected $VERSION, got $version" >&2 exit 1 fi echo "$container provenance verified: v$version $revision" done echo "Checking live health" retry=0 while [ "$retry" -lt 6 ]; do retry=$((retry + 1)) health_body="$(curl -fsS --max-time 10 "$BASE_URL/health" 2>/dev/null || true)" case "$health_body" in '{"status":"Healthy"'*) echo "Health check passed" break ;; esac if [ -n "$health_body" ]; then echo "Health endpoint is reachable but not healthy: $health_body" >&2 fi if [ "$retry" -eq 6 ]; then echo "Health check failed" >&2 exit 1 fi sleep "$retry" done pass=0 fail=0 check() { path="$1" expected="$2" label="$3" code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 "$BASE_URL$path")" printf '%-28s HTTP %s\n' "$label" "$code" if [ "$code" = "$expected" ]; then pass=$((pass + 1)) else fail=$((fail + 1)) fi } check_post() { path="$1" expected="$2" label="$3" code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' --data '{}' "$BASE_URL$path")" printf '%-28s HTTP %s\n' "$label" "$code" if [ "$code" = "$expected" ]; then pass=$((pass + 1)) else fail=$((fail + 1)) fi } check "/dashboard" "200" "Dashboard" check "/health" "200" "Health" check "/api/v1/operations/snapshot" "401" "Operations auth" check_post "/api/v1/chat" "401" "Chat auth" # ── Auth Smoke: SeedAudit owner_created exists in DB ── echo "" echo "Auth Smoke: SeedAudit owner_created" seed_key="$(docker exec nexus-postgres-1 psql -U nexus -d nexus -t -A -c "SELECT key FROM \"SeedAudit\" WHERE key = 'owner_created'" 2>/dev/null || echo "")" seed_key="$(echo "$seed_key" | tr -d '[:space:]')" if [ "$seed_key" = "owner_created" ]; then echo " SeedAudit owner_created: ✅ exists" pass=$((pass + 1)) else echo " SeedAudit owner_created: ❌ NOT FOUND (DB may not be seeded)" >&2 echo " Raw output: '$seed_key'" >&2 fail=$((fail + 1)) fi # ── Auth Smoke: Owner login flow returns 401 for unknown password ── # This proves the user exists, auth pipeline is functional, and the DB is reachable. # We POST with a WRONG password intentionally — a 401 means "user found, password wrong", # which is the correct auth flow behavior. A 5xx or connection error means the stack is broken. echo "Auth Smoke: Owner login flow" login_body="$(curl -sS --max-time 10 \ -X POST \ -H 'Content-Type: application/json' \ -d "{\"email\":\"${BOOTSTRAP_OWNER_EMAIL}\",\"password\":\"smoke-test-wrong-password-$(date +%s)\"}" \ "$BASE_URL/api/v1/auth/login" 2>/dev/null || echo "CONNECTION_ERROR")" if echo "$login_body" | grep -q '"error":"invalid_credentials"'; then echo " Owner login flow: ✅ HTTP 401 with valid JSON (auth pipeline working)" pass=$((pass + 1)) else echo " Owner login flow: ❌ unexpected response" >&2 echo " Response: $(echo "$login_body" | head -c 200)" >&2 fail=$((fail + 1)) fi if [ "$fail" -ne 0 ]; then echo "" echo "Smoke test failed: $fail failed, $pass passed" >&2 exit 1 fi echo "" echo "Nexus v$VERSION deployed and verified"