Skip to content

Commit 4f01a43

Browse files
Harden self-update with health-check and automatic rollback
The updater now snapshots the current commit before touching anything. If the build fails, or the API does not answer /health within ~60s after the PM2 reload, the checkout is reset back to that commit, rebuilt and reloaded — so a failed update can no longer leave the instance down. Reads API_PORT from .env to probe http://127.0.0.1:PORT/health (curl, then wget, then a raw TCP fallback).
1 parent a25aecf commit 4f01a43

1 file changed

Lines changed: 76 additions & 15 deletions

File tree

‎scripts/self-update.sh‎

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# detached, so it survives the PM2 reload it performs at the end). It resets the checkout to
55
# the tracked branch, rebuilds/migrates via deploy.sh, and reloads the PM2 processes.
66
#
7+
# SAFETY (hardened): the current commit is snapshotted *before* anything changes. If the build
8+
# fails, OR the API does not answer /health after the reload, the checkout is rolled back to that
9+
# snapshot, rebuilt and reloaded — so a bad update can no longer leave the instance down.
10+
#
711
# Progress is written to .update-status.json (read by the API) and full output to .update.log.
812
#
913
# NOTE: this discards local changes on the tracked branch with `git reset --hard`. Operators
@@ -24,31 +28,88 @@ write_status() {
2428
"$state" "$STARTED" "$finished" "$message" > "$STATUS"
2529
}
2630

27-
fail() {
28-
echo "FAILED: $1" >> "$LOG"
29-
write_status "failed" "$1" "\"$(date -Iseconds)\""
31+
# Read a single value from .env WITHOUT sourcing it (mirrors deploy.sh).
32+
read_env() {
33+
local key="$1" line val
34+
[[ -f .env ]] || { printf ''; return; }
35+
line="$(grep -E "^${key}=" .env | tail -1 || true)"
36+
val="${line#*=}"
37+
[[ "$val" == \"*\" ]] && val="${val:1:${#val}-2}"
38+
[[ "$val" == \'*\' ]] && val="${val:1:${#val}-2}"
39+
printf '%s' "$val"
40+
}
41+
42+
API_PORT="$(read_env API_PORT)"; : "${API_PORT:=4000}"
43+
HEALTH_URL="http://127.0.0.1:${API_PORT}/health"
44+
45+
reload_pm2() {
46+
if command -v pm2 >/dev/null 2>&1; then
47+
{ pm2 reload "$ROOT/ecosystem.config.cjs"; } >> "$LOG" 2>&1 || echo "pm2 reload warning" >> "$LOG"
48+
else
49+
echo "pm2 not found on PATH — reload skipped; restart the processes manually." >> "$LOG"
50+
fi
51+
}
52+
53+
# True once the API answers its /health endpoint. Tries curl, then wget, then a raw TCP probe.
54+
http_ok() {
55+
if command -v curl >/dev/null 2>&1; then
56+
curl -fsS --max-time 4 "$HEALTH_URL" >/dev/null 2>&1 && return 0
57+
return 1
58+
fi
59+
if command -v wget >/dev/null 2>&1; then
60+
wget -q -T 4 -O /dev/null "$HEALTH_URL" >/dev/null 2>&1 && return 0
61+
return 1
62+
fi
63+
# Last resort: just check the port accepts a connection.
64+
(exec 3<>"/dev/tcp/127.0.0.1/${API_PORT}") 2>/dev/null && return 0
65+
return 1
66+
}
67+
68+
# Poll /health for up to ~60s (30 × 2s). PM2 reload is graceful, so give the new process time.
69+
wait_healthy() {
70+
local i
71+
for i in $(seq 1 30); do
72+
if http_ok; then return 0; fi
73+
sleep 2
74+
done
75+
return 1
76+
}
77+
78+
# Roll the checkout back to the pre-update commit and bring it back up. Best-effort.
79+
rollback() {
80+
local reason="$1"
81+
echo "ROLLBACK ($reason) → $PREV_SHA" >> "$LOG"
82+
write_status "running" "Échec — restauration de la version précédente…" "null"
83+
{ git reset --hard "$PREV_SHA"; } >> "$LOG" 2>&1
84+
{ ./scripts/deploy.sh; } >> "$LOG" 2>&1 || echo "rollback rebuild warning" >> "$LOG"
85+
reload_pm2
86+
write_status "failed" "Mise à jour annulée et version précédente restaurée ($reason)." "\"$(date -Iseconds)\""
87+
echo "== rolled back $(date -Iseconds) ==" >> "$LOG"
3088
exit 1
3189
}
3290

3391
: > "$LOG"
3492
echo "== self-update $(date -Iseconds) (branch $BRANCH) ==" >> "$LOG"
3593
write_status "running" "Récupération des sources…" "null"
3694

37-
{ git fetch --all --prune; } >> "$LOG" 2>&1 || fail "git fetch a échoué"
38-
{ git checkout "$BRANCH"; } >> "$LOG" 2>&1 || fail "git checkout $BRANCH a échoué"
39-
{ git reset --hard "origin/$BRANCH"; } >> "$LOG" 2>&1 || fail "git reset a échoué"
95+
# Snapshot the current commit so we can return to it if anything goes wrong.
96+
PREV_SHA="$(git rev-parse HEAD 2>/dev/null || echo '')"
97+
[[ -n "$PREV_SHA" ]] || { write_status "failed" "Impossible de lire le commit courant (pas un dépôt git ?)." "\"$(date -Iseconds)\""; exit 1; }
98+
echo "snapshot PREV_SHA=$PREV_SHA" >> "$LOG"
4099

41-
write_status "running" "Build et migrations…" "null"
42-
{ ./scripts/deploy.sh; } >> "$LOG" 2>&1 || fail "le build/déploiement a échoué"
100+
{ git fetch --all --prune; } >> "$LOG" 2>&1 || { write_status "failed" "git fetch a échoué." "\"$(date -Iseconds)\""; exit 1; }
101+
{ git checkout "$BRANCH"; } >> "$LOG" 2>&1 || { write_status "failed" "git checkout $BRANCH a échoué." "\"$(date -Iseconds)\""; exit 1; }
102+
{ git reset --hard "origin/$BRANCH"; } >> "$LOG" 2>&1 || rollback "git reset a échoué"
43103

44-
# Mark success before reloading: the reload restarts this very API process, which then reads
45-
# the status file. The reload itself is best-effort.
46-
write_status "success" "Mise à jour appliquée, redémarrage…" "\"$(date -Iseconds)\""
104+
write_status "running" "Build et migrations…" "null"
105+
{ ./scripts/deploy.sh; } >> "$LOG" 2>&1 || rollback "le build/déploiement a échoué"
47106

48-
if command -v pm2 >/dev/null 2>&1; then
49-
{ pm2 reload "$ROOT/ecosystem.config.cjs"; } >> "$LOG" 2>&1 || echo "pm2 reload warning" >> "$LOG"
50-
else
51-
echo "pm2 not found on PATH — reload skipped; restart the processes manually." >> "$LOG"
107+
# Reload, then verify the API actually comes back before declaring success.
108+
write_status "running" "Redémarrage et vérification de l'état…" "null"
109+
reload_pm2
110+
if ! wait_healthy; then
111+
rollback "l'API ne répond plus après le redémarrage"
52112
fi
53113

114+
write_status "success" "Mise à jour appliquée et vérifiée." "\"$(date -Iseconds)\""
54115
echo "== done $(date -Iseconds) ==" >> "$LOG"

0 commit comments

Comments
 (0)