Files
codexpi/scripts/redeploy.sh

121 lines
5.3 KiB
Bash
Raw Normal View History

2026-11-14 20:23:59 +01:00
#!/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."