make a deploy fail loudly when the service never restarted

سرور دو روز کدِ قدیمی را سرو کرد و هیچ‌کس نفهمید. علتش این بود که
`systemctl restart` شکست خورده بود ولی پروسه‌ی قبلی هنوز بالا بود و همان
۲۰۰ را می‌داد — پس اسکریپت «✓ سرویس سالم بالا آمد» چاپ کرد و با کدِ صفر
تمام شد. محتوا و فایل‌های استاتیک هم روی دیسک بودند و سرو نمی‌شدند.

حالا زمانِ بالا آمدنِ سرویس قبل و بعد مقایسه می‌شود. اگر عوض نشده باشد،
۲۰۰ گرفتن دیگر موفقیت حساب نمی‌شود و اسکریپت با خطا تمام می‌کند.

نامِ عامِ `gunicorn` هم از فهرستِ حدسِ سرویس برداشته شد: روی سرورِ واقعی یک
`gunicorn.service` هست که مالِ تیمِ دیگری است، و حدس زدنش یعنی ری‌استارت
کردنِ سرویسِ یک نفرِ دیگر. نامِ درستِ ما `gunicorn-mathapp` است.

`api/health/` هم کنارِ `api/health` ثبت شد. پایشِ سرور شکلِ اسلش‌دار را صدا
می‌زد و چون ثبت نبود، کسی روی سرور `config/urls.py` را دستی ویرایش کرده
بود — و از آن به بعد هر `git pull` روی همان فایل گیر می‌کرد.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 22:56:51 +03:30
parent 07fab4c094
commit de118db58f
3 changed files with 94 additions and 4 deletions

View File

@@ -33,7 +33,9 @@ if [ -n "${TADRISINO_HEALTH_URL:-}" ]; then
else
HEALTH_URLS=(
"http://127.0.0.1:8000/api/health"
"http://127.0.0.1:8000/api/health/"
"https://api.math.tadrisino2.ir/api/health"
"https://api.math.tadrisino2.ir/api/health/"
)
fi
@@ -150,18 +152,36 @@ SERVICE="${TADRISINO_SERVICE:-}"
if [ -z "$SERVICE" ] && command -v systemctl >/dev/null 2>&1; then
# `systemctl cat` تنها بررسیِ قابلِ اتکاست: list-unit-files حتی وقتی چیزی
# پیدا نکند هم کدِ ۰ برمی‌گرداند.
for candidate in tadrisino tadrisino-api mathapp gunicorn; do
# عمداً نامِ عامِ `gunicorn` در این فهرست نیست. روی سرورِ واقعی چند اپِ
# مختلف کنارِ هم هستند و یک `gunicorn.service` وجود دارد که مالِ تیمِ
# دیگری است — حدس زدنِ آن یعنی ری‌استارت کردنِ سرویسِ یک نفرِ دیگر. اگر
# نامِ سرویس در این فهرست نبود، TADRISINO_SERVICE را صریح بدهید.
for candidate in tadrisino tadrisino-api gunicorn-mathapp mathapp; do
if systemctl cat "${candidate}.service" >/dev/null 2>&1; then
SERVICE="$candidate"; break
fi
done
fi
RESTART_OK=0
if [ -n "$SERVICE" ]; then
# شکستِ ری‌استارت نباید اسکریپت را بی‌صدا تمام کند؛ بررسیِ سلامتِ پایین
# همچنان باید اجرا شود تا معلوم شود سرویسِ قبلی هنوز بالاست یا نه.
# زمانِ بالا آمدنِ سرویس قبل و بعد مقایسه می‌شود.
#
# چرا: قبلاً فقط خروجیِ `systemctl restart` نگاه می‌شد و بعد سلامت از روی
# آدرسِ عمومی چک می‌شد. ولی وقتی ری‌استارت شکست می‌خورد، پروسه‌ی *قدیمی*
# هنوز بالا بود و همان ۲۰۰ را می‌داد — یعنی اسکریپت «✓ سالم» می‌گفت در
# حالی که کدِ جدید اصلاً بار نشده بود. روی سرور دو روز همین‌طور ماند: کد و
# محتوا و فایل‌های استاتیک روی دیسک بودند و هیچ‌کدام سرو نمی‌شدند.
BEFORE="$(systemctl show "$SERVICE" -p ActiveEnterTimestamp --value 2>/dev/null || true)"
if sudo systemctl restart "$SERVICE"; then
printf ' سرویسِ %s ری‌استارت شد\n' "$SERVICE"
AFTER="$(systemctl show "$SERVICE" -p ActiveEnterTimestamp --value 2>/dev/null || true)"
if [ -n "$BEFORE" ] && [ "$BEFORE" = "$AFTER" ]; then
warn "دستورِ ری‌استارت خطا نداد ولی سرویس بالا نیامد (هنوز از $BEFORE در حال اجراست).
یعنی کدِ جدید بار نشده. لاگ: sudo journalctl -u $SERVICE -n 50 --no-pager"
else
RESTART_OK=1
printf ' سرویسِ %s ری‌استارت شد\n' "$SERVICE"
fi
else
warn "ری‌استارتِ $SERVICE شکست خورد (شاید sudo رمز می‌خواهد). خودتان اجرا کنید:
sudo systemctl restart $SERVICE"
@@ -185,6 +205,12 @@ for attempt in 1 2 3 4 5 6 7 8 9 10; do
if [ "$CODE" = "200" ]; then
printf '%s✓ سرویس سالم بالا آمد%s (%s)\n' "$C_OK" "$C_OFF" "$url"
printf '%s\n' "$BODY" | python -m json.tool 2>/dev/null | sed 's/^/ /' || printf ' %s\n' "$BODY"
# ۲۰۰ گرفتن به‌تنهایی یعنی «چیزی جواب می‌دهد»، نه «کدِ جدید بالاست».
if [ "$RESTART_OK" != "1" ]; then
warn "ولی سرویس ری‌استارت نشد — این جوابْ از پروسه‌ی قدیمی است و کدِ
جدید هنوز سرو نمی‌شود. تا ری‌استارت نکنید دیپلوی ناتمام است."
exit 1
fi
exit 0
fi
done