- Supprime rm -f (causait la perte de ~23 vars a chaque deploy) - upsert ecrit KEY=value sans quotes (compatible Docker Compose v2) - CRLF strip avant ecriture (sed s/\r$//) - Sanity-check post-upsert: abort si NEXTAUTH_SECRET/AUTH_GOOGLE_ID/etc manquantes - Header ## AUTO-MANAGED BY CI ## en tete de fichier genere - deploy-prod.sh: sanity-check pre-deploy (NEXTAUTH_URL/SECRET/GOOGLE_ID/SECRET) - Ajoute .env.docker.example (reference complete de toutes les vars) - Ajoute MCP_SERVER_MODE/MCP_SERVER_URL manquantes dans deploy.yaml
253 lines
9.3 KiB
Bash
Executable File
253 lines
9.3 KiB
Bash
Executable File
#!/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"
|
||
|
||
# Pre-deploy sanity-check: .env.docker must have critical vars
|
||
if [ -f "$ROOT/.env.docker" ]; then
|
||
sed -i 's/\r$//' "$ROOT/.env.docker"
|
||
for required in NEXTAUTH_URL NEXTAUTH_SECRET AUTH_GOOGLE_ID AUTH_GOOGLE_SECRET; do
|
||
grep -q "^${required}=" "$ROOT/.env.docker" || {
|
||
echo "ERROR: $required missing in .env.docker — aborting deploy"
|
||
telegram_notify "failure" "Missing $required in .env.docker"
|
||
exit 1
|
||
}
|
||
done
|
||
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
|