#!/usr/bin/env bash # Déploiement prod sur 192.168.1.190 — exécuter SUR LE SERVEUR (runner docker-host). set -euo pipefail # Telegram notification function telegram_notify() { local status="$1" local message="$2" local bot_token="${TELEGRAM_BOT_TOKEN:-}" local chat_id="${TELEGRAM_CHAT_ID:-}" if [ -z "$bot_token" ] || [ -z "$chat_id" ]; then echo "TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set — skipping notification" return 0 fi local emoji="✅" if [ "$status" = "failure" ]; then emoji="❌" elif [ "$status" = "rollback" ]; then emoji="🔄" fi local commit_msg=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "unknown") # Escape markdown special characters in commit message commit_msg=$(echo "$commit_msg" | sed 's/[_*`[\]()~>#+=|{}.!\\-]/\\&/g') local commit_short="${GIT_COMMIT:0:8}" curl -s -X POST "https://api.telegram.org/bot${bot_token}/sendMessage" \ -d "chat_id=${chat_id}" \ -d "text=${emoji} Momento Deploy ${status} Commit: ${commit_short} Message: ${commit_msg} Details: ${message}" \ -d "parse_mode=Markdown" > /dev/null 2>&1 || echo "Telegram notification failed" } # Rollback functions rollback_save_image() { echo "=== Saving current image for rollback ===" if docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "^memento-memento-note:latest"; then docker tag memento-memento-note:latest memento-memento-note:rollback || echo "WARN: Failed to tag rollback image" echo "Rollback image saved: memento-memento-note:rollback" else echo "WARN: No current memento-note image found to save" fi } rollback_restore_image() { echo "=== Rolling back to previous image ===" if docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "^memento-memento-note:rollback"; then docker tag memento-memento-note:rollback memento-memento-note:latest || echo "ERROR: Failed to restore rollback image" docker compose up -d --force-recreate memento-note || echo "ERROR: Failed to restart container" echo "Rollback completed" else echo "ERROR: No rollback image found" return 1 fi } ROOT="${DEPLOY_ROOT:-/opt/memento}" ARTIFACT_TGZ="${ARTIFACT_TGZ:-}" EXPECTED_COMMIT="${EXPECTED_COMMIT:-}" load_env_docker() { local env_file="$ROOT/.env.docker" [ -f "$env_file" ] || return 0 # Parse ligne par ligne (ne crashe pas sur une valeur avec quote mal fermée, # contrairement à `source` qui exécute le fichier comme du bash). local line key val while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|\#*) continue ;; # vide / commentaire *=*) ;; # doit contenir = *) continue ;; esac key="${line%%=*}" val="${line#*=}" # strip les guillemets entourants seulement s'ils sont équilibrés case "$val" in \"*\") val="${val#\"}"; val="${val%\"}" ;; \'*\') val="${val#\'}"; val="${val%\'}" ;; esac # valide le nom de variable (alnum + underscore, commence par une lettre) case "$key" in [A-Za-z_]*|[A-Za-z_]*) export "$key=$val" ;; esac done < "$env_file" } wait_for_postgres() { local pg_user="${POSTGRES_USER:-memento}" local pg_db="${POSTGRES_DB:-memento}" if docker compose up -d --wait postgres 2>/dev/null; then echo "Postgres healthy (compose --wait)" return 0 fi docker compose up -d postgres for i in $(seq 1 30); do # Use credentials from inside the container (authoritative) if docker compose exec -T postgres sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1; then echo "Postgres ready (${i}/30)" return 0 fi # Fallback: host env after load_env_docker if docker compose exec -T postgres pg_isready -U "$pg_user" -d "$pg_db" >/dev/null 2>&1; then echo "Postgres ready via host env (${i}/30)" return 0 fi sleep 2 done echo "Postgres not ready after 60s" docker compose ps postgres 2>/dev/null || true docker compose logs postgres --tail=50 2>/dev/null || true docker compose exec -T postgres sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' 2>&1 || true return 1 } # Health check configuration: 24 iterations × 5 seconds = 2 minutes total timeout # This allows Next.js cold start time while keeping feedback fast HEALTH_CHECK_MAX_ITERATIONS=24 HEALTH_CHECK_SLEEP_SECONDS=5 cd "$ROOT" if [ -f "$ROOT/.env.docker" ]; then sed -i 's/\r$//' "$ROOT/.env.docker" fi load_env_docker git config --global --add safe.directory "$ROOT" 2>/dev/null || true git fetch origin main git reset --hard origin/main export GIT_COMMIT="$(git rev-parse HEAD)" echo "=== Deploy $GIT_COMMIT ===" if [ -n "$EXPECTED_COMMIT" ] && [ "$GIT_COMMIT" != "$EXPECTED_COMMIT" ]; then echo "ERROR: git HEAD $GIT_COMMIT != expected $EXPECTED_COMMIT" exit 1 fi wait_for_postgres || exit 1 docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION IF NOT EXISTS vector;"' >/dev/null if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^memento-note/prisma/migrations/'; then DUMP_FILE="/opt/memento/backups/pre-migrate-$(date +%Y%m%d-%H%M%S).sql.gz" mkdir -p /opt/memento/backups docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-memento}" -d "${POSTGRES_DB:-memento}" | gzip > "$DUMP_FILE" DUMP_SIZE=$(stat -c%s "$DUMP_FILE") if [ "$DUMP_SIZE" -lt 1048576 ]; then echo "ERROR: Dump too small ($DUMP_SIZE bytes)" exit 1 fi echo "Backup: $DUMP_FILE ($(( DUMP_SIZE / 1024 ))KB)" else echo "No migration change — skip pre-migrate dump" fi export COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 export GIT_COMMIT # Save current image for rollback before building new one rollback_save_image if [ -n "$ARTIFACT_TGZ" ] && [ -f "$ARTIFACT_TGZ" ]; then echo "=== Fast image (CI artifact) ===" tar xzf "$ARTIFACT_TGZ" -C memento-note/ export MEMENTO_DOCKERFILE=Dockerfile.prebuilt export MEMENTO_SOCKET_DOCKERFILE=Dockerfile.socket.prebuilt else echo "ERROR: No CI artifact at $ARTIFACT_TGZ — full Docker build is not supported on this host." telegram_notify "failure" "Deploy aborted: missing prebuilt artifact" exit 1 fi docker compose build memento-note docker compose build memento-socket if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^mcp-server/'; then docker compose build mcp-server fi docker compose up -d --remove-orphans --force-recreate memento-note docker compose up -d memento-socket docker compose up -d mcp-server 2>/dev/null || true # Monitoring stack updates are handled at the end of successful deployment echo "=== Migrations (Prisma CLI via node, pas npx) ===" if docker compose exec -T memento-note test -f ./node_modules/prisma/build/index.js 2>/dev/null; then docker compose exec -T memento-note node ./node_modules/prisma/build/index.js migrate deploy else echo "WARN: prisma CLI absent de l'image — les migrations tournent au démarrage (entrypoint) ou via psql manuel" fi nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || true for i in $(seq 1 "$HEALTH_CHECK_MAX_ITERATIONS"); do BODY=$(docker compose exec -T memento-note node -e "require('http').get('http://localhost:3000/api/build-info',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{console.log(d);process.exit(0)})}).on('error',()=>process.exit(1))" 2>/dev/null || true) ACTUAL=$(echo "$BODY" | jq -r '.commit // empty' 2>/dev/null || true) if [ "$ACTUAL" = "$GIT_COMMIT" ]; then echo "OK build-info commit=$ACTUAL" echo "=== Updating monitoring stack ===" # Fix potential Docker directory/file mount corruption for metrics-token if [ -d monitoring/metrics-token ]; then echo "WARN: monitoring/metrics-token is a directory on host, removing it" rm -rf monitoring/metrics-token git checkout monitoring/metrics-token || echo "default-token-value" > monitoring/metrics-token fi load_env_docker if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${TELEGRAM_CHAT_ID:-}" ]; then echo "=== Starting Monitoring Stack (with Telegram bot) ===" docker compose -f monitoring/docker-compose.monitoring.yml --profile telegram up -d --remove-orphans 2>&1 || echo "WARN: Failed to bring up monitoring stack" else echo "=== Starting Monitoring Stack (without Telegram bot) ===" docker compose -f monitoring/docker-compose.monitoring.yml up -d --remove-orphans 2>&1 || echo "WARN: Failed to bring up monitoring stack" fi # Force recreation of Grafana, Prometheus and Alertmanager to apply config changes and fix mounts echo "=== Recreating Grafana, Prometheus and Alertmanager containers ===" docker compose -f monitoring/docker-compose.monitoring.yml up -d --force-recreate grafana prometheus alertmanager 2>&1 || true docker compose ps telegram_notify "success" "Deployment successful — app is healthy" exit 0 fi sleep "$HEALTH_CHECK_SLEEP_SECONDS" done echo "ERROR: build-info=$ACTUAL expected=$GIT_COMMIT" docker logs memento-web --tail=40 # Rollback on health check failure rollback_restore_image telegram_notify "rollback" "Health check failed — rolled back to previous version" exit 1