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 "
3492echo " == self-update $( date -Iseconds) (branch $BRANCH ) ==" >> " $LOG "
3593write_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"
52112fi
53113
114+ write_status " success" " Mise à jour appliquée et vérifiée." " \" $( date -Iseconds) \" "
54115echo " == done $( date -Iseconds) ==" >> " $LOG "
0 commit comments