-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·132 lines (116 loc) · 5.99 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·132 lines (116 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env bash
#
# Le Canard du Vendredi — déploiement applicatif (push-to-deploy, §31.5).
#
# Déclenché par `git push main` : le webhook fait `git reset --hard origin/main`
# puis exécute ce script. Ordre IMPÉRATIF (rollback prêt AVANT toute bascule) :
#
# 0. préserver l'image en cours sous :rollback (filet, AVANT le build)
# 1. dump PostgreSQL pré-migration (filet base)
# 2. build de l'image prod (assets + .env.local.php)
# 3. base up + migrations Doctrine (gate) (--all-or-nothing)
# 4. app + worker + relay up
# 5. READINESS HTTP BLOQUANTE (/health/ready) (rouge → rollback auto + exit 1)
#
# Idempotent et sûr à rejouer. NE bascule JAMAIS le trafic avant /health/ready vert.
set -euo pipefail
cd "$(dirname "$0")"
# ── Configuration (surchargeables pour la répétition locale) ──────────────────
ENV_FILE="${ENV_FILE:-.env.prod.local}"
COMPOSE_FILE="${COMPOSE_FILE:-compose.prod.yaml}"
IMAGE="${IMAGE:-friday-duck/app}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-150}" # secondes avant d'abandonner le boot
BACKUP_DIR="${BACKUP_DIR:-backups}"
log() { printf '\033[1;36m▶ %s\033[0m\n' "$*"; }
ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
err() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
if [ ! -f "$ENV_FILE" ]; then
err "Fichier de secrets '$ENV_FILE' absent. Sur le VPS : cp .env.prod.local.dist $ENV_FILE puis renseigner les secrets."
exit 1
fi
APP_VERSION="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)"
export APP_VERSION
log "Déploiement de la version ${APP_VERSION}"
# ── 0. Rollback prêt AVANT : préserver l'image actuellement déployée ──────────
ROLLBACK_AVAILABLE=0
if docker image inspect "${IMAGE}:latest" >/dev/null 2>&1; then
docker tag "${IMAGE}:latest" "${IMAGE}:rollback"
ROLLBACK_AVAILABLE=1
ok "Image courante préservée sous ${IMAGE}:rollback"
else
log "Pas d'image précédente (premier déploiement) : aucun rollback d'image disponible."
fi
rollback() {
err "Déploiement ÉCHOUÉ — rollback en cours…"
if [ "$ROLLBACK_AVAILABLE" = 1 ]; then
docker tag "${IMAGE}:rollback" "${IMAGE}:latest"
compose up -d --no-build app worker relay
err "Rollback effectué : image précédente redéployée. Inspecter les logs (compose logs app)."
else
err "Aucune image de rollback (premier déploiement) — stack laissée arrêtée. Corriger puis redéployer."
compose down || true
fi
}
# ── 1. Filet base : dump PostgreSQL avant migrations ──────────────────────────
# La base doit tourner pour être dumpée ; on la démarre d'abord.
log "Démarrage de la base de données…"
compose up -d database
# Attendre que PostgreSQL accepte les connexions (healthcheck du service).
db_cid="$(compose ps -q database)"
for _ in $(seq 1 30); do
[ "$(docker inspect -f '{{.State.Health.Status}}' "$db_cid" 2>/dev/null || echo starting)" = healthy ] && break
sleep 2
done
mkdir -p "$BACKUP_DIR"
DUMP_FILE="${BACKUP_DIR}/pre-migrate-$(date +%Y%m%d-%H%M%S).sql"
# shellcheck disable=SC2016 # $POSTGRES_* doivent s'expandre DANS le conteneur, pas ici.
if compose exec -T database sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > "$DUMP_FILE" 2>/dev/null && [ -s "$DUMP_FILE" ]; then
ok "Dump pré-migration : ${DUMP_FILE} ($(wc -c < "$DUMP_FILE") octets)"
else
rm -f "$DUMP_FILE"
log "Pas de dump (base vide ou inexistante — premier déploiement)."
fi
# ── 2. Build de l'image prod (assets compilés + config env figée) ─────────────
log "Build de l'image de production…"
compose build --build-arg APP_VERSION="$APP_VERSION" app
# ── 3. Migrations Doctrine — GATE explicite, AVANT la mise en service ─────────
# Conteneur jetable, --no-deps (la base tourne déjà), --all-or-nothing :
# l'échec d'une migration STOPPE le déploiement (aucune bascule).
log "Migrations Doctrine…"
if ! compose run --rm --no-deps app php bin/console doctrine:migrations:migrate \
--no-interaction --all-or-nothing --allow-no-migration; then
err "Migrations en échec — restauration possible depuis ${DUMP_FILE:-<aucun dump>}."
rollback
exit 1
fi
ok "Migrations appliquées."
# ── 4. Mise en service : app (web + hub Mercure) + worker + relais outbox ─────
log "Démarrage de l'application, du worker et du relais…"
compose up -d app worker relay
# ── 5. READINESS HTTP BLOQUANTE (/health/ready : base incluse) ────────────────
# On NE lit PLUS le statut Docker : la sonde Docker récurrente est une LIVENESS
# (/health, sans base) pour éviter les restart-loops si la base flanche en prod.
# Le GATE de déploiement, lui, EXIGE la base : on interroge /health/ready
# (SELECT 1) via le vhost interne. file_get_contents → false sur 503/refus.
log "Attente de la readiness (/health/ready, base incluse) — timeout ${HEALTH_TIMEOUT}s…"
elapsed=0
ready=0
while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do
# shellcheck disable=SC2016 # code PHP : ne pas laisser le shell hôte interpréter l'appel.
if compose exec -T app php -r 'exit(@file_get_contents("http://app/health/ready") === false ? 1 : 0);' >/dev/null 2>&1; then
ready=1; break
fi
sleep 3; elapsed=$((elapsed + 3))
done
if [ "$ready" != 1 ]; then
err "Readiness ROUGE après ${HEALTH_TIMEOUT}s (/health/ready ≠ 200 — base injoignable ?)."
compose logs --tail=50 app >&2 || true
rollback
exit 1
fi
# Confirmation lisible du corps de /health/ready (statut + base + version).
# shellcheck disable=SC2016 # code PHP : ne pas laisser le shell hôte interpréter $r.
compose exec -T app php -r '$r=@file_get_contents("http://app/health/ready"); echo $r ? $r.PHP_EOL : "no body".PHP_EOL;' || true
ok "Readiness VERTE — version ${APP_VERSION} en ligne."
log "Smoke test : ./scripts/smoke-test.sh (valide le comportement réel sur le domaine public)"