#!/usr/bin/env bash set -euo pipefail BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$BASE" # The Codex container can mount its default Docker config read-only. Keep build # metadata in a disposable location so a deployment never fails for that reason. export DOCKER_CONFIG="${CODEXPI_DOCKER_CONFIG:-/tmp/codexpi-docker-config}" mkdir -p "$DOCKER_CONFIG" command -v docker >/dev/null || { echo "Docker est requis." >&2; exit 1; } if ! docker info >/dev/null 2>&1; then socket_gid="$(stat -c '%g' /var/run/docker.sock 2>/dev/null || true)" if [[ -n "$socket_gid" && " $(id -G) " != *" $socket_gid "* ]]; then echo "Docker n’est pas accessible : le processus courant n’appartient pas au groupe $socket_gid du socket." >&2 echo "Depuis l’hôte, installez le service de boot puis relancez ce script pour recréer l’API avec le bon groupe :" >&2 echo " ./scripts/install-boot-service.sh" >&2 echo " ./scripts/redeploy.sh" >&2 else echo "Docker n’est pas accessible." >&2 fi exit 1 fi docker_gid="$(stat -c '%g' /var/run/docker.sock)" if docker ps --format '{{.Names}}' | grep -q '^codexpi-safe-redeploy-'; then echo "Un déploiement CodexPi est déjà en cours ; aucune seconde bascule ne sera lancée." exit 0 fi if docker inspect codexpi-api-next >/dev/null 2>&1 || docker inspect codexpi-django-next >/dev/null 2>&1; then echo "Une version de relève est encore active. Vérifiez-la avant de relancer un déploiement." >&2 exit 1 fi stamp="$(date +%s)" image="codexpi-app:deploy-$stamp" echo "Construction isolée de la nouvelle image CodexPi ($image)…" docker build --target production -t "$image" "$BASE" helper="codexpi-safe-redeploy-$stamp" docker run -d --rm --name "$helper" \ -e CODEXPI_DEPLOY_IMAGE="$image" \ -e DOCKER_GID="$docker_gid" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$BASE:$BASE" -w "$BASE" \ docker:cli sh -c ' set -eu api_next=codexpi-api-next django_next=codexpi-django-next switched=0 cleanup() { if [ "$switched" = 0 ]; then docker rm -f "$api_next" "$django_next" >/dev/null 2>&1 || true fi } trap cleanup EXIT wait_healthy() { container="$1" attempts=0 while [ "$attempts" -lt 60 ]; do health=$(docker inspect -f "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}" "$container" 2>/dev/null || true) [ "$health" = healthy ] && return 0 attempts=$((attempts + 1)) sleep 2 done echo "Le conteneur $container n’est pas devenu sain." >&2 return 1 } wait_drained() { container="$1" quiet=0 echo "Drainage de $container : les discussions en cours restent actives…" # Five consecutive quiet checks are enough to confirm that the old API no # longer owns a live session, without delaying every deployment by five # minutes once the last worker has exited. while [ "$quiet" -lt 5 ]; do if docker top "$container" -eo pid,args 2>/dev/null | grep -Eq "codexpi\.session_worker|/codex .* exec"; then quiet=0 else quiet=$((quiet + 1)) fi sleep 2 done } echo "Démarrage de la nouvelle version en parallèle…" docker rm -f "$api_next" "$django_next" >/dev/null 2>&1 || true CODEXPI_IMAGE="$CODEXPI_DEPLOY_IMAGE" docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" run -d --no-deps --name "$api_next" -e CODEXPI_SKIP_WORKER_RECOVERY=true api >/dev/null CODEXPI_IMAGE="$CODEXPI_DEPLOY_IMAGE" docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" run -d --no-deps --name "$django_next" django-web >/dev/null wait_healthy "$api_next" wait_healthy "$django_next" echo "Bascule Nginx vers la nouvelle version saine…" printf "upstream django_ui { server %s:8001; }\\nupstream fastapi_api { server %s:8000; }\\n" "$django_next" "$api_next" | docker exec -i codexpi-nginx-1 sh -c "cat > /tmp/codexpi-upstreams.conf" docker exec codexpi-nginx-1 nginx -t docker exec codexpi-nginx-1 nginx -s reload switched=1 wait_drained codexpi-api-1 echo "Remplacement de l’ancienne version désormais inactive…" # Promote only the image which already passed both health checks. The # stable tag is what systemd/docker compose will reuse after a host reboot. docker image tag "$CODEXPI_DEPLOY_IMAGE" codexpi-app:local CODEXPI_IMAGE=codexpi-app:local docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" up -d --no-deps --force-recreate api django-web wait_healthy codexpi-api-1 wait_healthy codexpi-django-web-1 echo "Retour vers les services principaux et nettoyage…" printf "upstream django_ui { server django-web:8001; }\\nupstream fastapi_api { server api:8000; }\\n" | docker exec -i codexpi-nginx-1 sh -c "cat > /tmp/codexpi-upstreams.conf" docker exec codexpi-nginx-1 nginx -t docker exec codexpi-nginx-1 nginx -s reload wait_drained "$api_next" docker rm -f "$api_next" "$django_next" >/dev/null switched=0 trap - EXIT echo "Redéploiement terminé sans coupure." ' >/dev/null echo "Image construite. Déploiement blue/green programmé ($helper)." echo "Les anciennes connexions resteront actives jusqu’à la fin de leur discussion."