make a deploy observable and one command instead of a checklist

بالا آوردنِ نسخه‌ی جدید تا الان چند کارِ دستی به ترتیبِ درست بود (pull، نصبِ
وابستگی‌ها، مهاجرت، استاتیک، ری‌استارت) و فراموش شدنِ هر کدام اپ را می‌خواباند
با خطایی که از بیرون فقط «502 Bad Gateway» دیده می‌شد. همین الان هم سرور
دقیقاً به همین دلیل خوابیده: کامیتِ ac1b1af پکیجِ django-cors-headers را اضافه
کرد، و بدونِ pip install جنگو با ModuleNotFoundError بالا نمی‌آید.

deploy.sh همه را به ترتیب و idempotent انجام می‌دهد. pip install همیشه اجرا
می‌شود، نه فقط وقتی requirements عوض شده — تشخیصِ «عوض شده یا نه» خودش یک جای
فراموش‌شدنیِ دیگر است. آخرِ کار خودش سلامت را بررسی می‌کند و اگر بالا نیامده
باشد علت‌های رایج را چاپ می‌کند، به‌جای اینکه کسی بعداً از بیرون کشفش کند.

GET /api/health وضعیت را بدونِ احراز و بدونِ SSH بیرون می‌دهد: دیتابیس،
مهاجرت‌های اعمال‌نشده، کش، و شمارشِ پایه/درس/سوال روی همان سرور. مهم‌ترینش
مهاجرتِ اعمال‌نشده است — حالتی که اپ ظاهراً بالا می‌آید و بعد زیرِ بارِ واقعی
با خطای ستونِ ناموجود می‌شکند. عمداً هیچ سکرتی برنمی‌گرداند، فقط بله/خیرِ
«تنظیم شده».

۵۰۳ برمی‌گرداند وقتی دیتابیس/کش/مهاجرت خراب است تا پایشِ خودکار بتواند رویش
حساب کند؛ نبودِ محتوا خرابی حساب نمی‌شود ولی گزارش می‌شود.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 20:28:59 +03:30
parent 5f3aae0756
commit cbe9b63dc4
5 changed files with 311 additions and 0 deletions

134
deploy.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
#
# دیپلویِ تدریسینو — یک دستور، نه یک فهرستِ کار.
#
# ./deploy.sh
#
# چرا این فایل وجود دارد: تا امروز بالا آوردنِ نسخه‌ی جدید یعنی چند کارِ دستی
# به ترتیبِ درست (pull، نصبِ وابستگی‌ها، مهاجرت، فایل‌های استاتیک، ری‌استارت).
# فراموش شدنِ هر کدام، اپ را می‌خواباند و خطایش هم گویا نیست — مثلاً اضافه شدنِ
# یک پکیجِ جدید بدونِ `pip install` باعثِ ModuleNotFoundError موقعِ بالا آمدن
# می‌شود و کاربر فقط «502 Bad Gateway» می‌بیند. این اسکریپت همه را به ترتیب و
# idempotent انجام می‌دهد و آخرش خودش بررسی می‌کند که واقعاً بالا آمده یا نه.
#
# متغیرهای اختیاری:
# TADRISINO_SERVICE نامِ سرویسِ systemd برای ری‌استارت (مثلاً tadrisino)
# TADRISINO_HEALTH_URL آدرسی که در پایان بررسی می‌شود
# SKIP_PULL=1 بدونِ git pull (وقتی کد را دستی گذاشته‌اید)
set -euo pipefail
cd "$(dirname "$0")"
C_OK=$'\033[0;32m'; C_ERR=$'\033[0;31m'; C_WARN=$'\033[0;33m'; C_OFF=$'\033[0m'
step() { printf '\n%s▸ %s%s\n' "$C_OK" "$1" "$C_OFF"; }
warn() { printf '%s⚠ %s%s\n' "$C_WARN" "$1" "$C_OFF"; }
fail() { printf '%s✖ %s%s\n' "$C_ERR" "$1" "$C_OFF" >&2; exit 1; }
HEALTH_URL="${TADRISINO_HEALTH_URL:-http://127.0.0.1:8000/api/health}"
# ─── ۱. کدِ تازه ────────────────────────────────────────────────────────────
if [ "${SKIP_PULL:-0}" != "1" ]; then
step "گرفتنِ کدِ تازه"
git pull --ff-only || fail "git pull نشد. اگر تغییرِ محلی دارید اول تعیین تکلیفش کنید."
else
step "گرفتنِ کد رد شد (SKIP_PULL=1)"
fi
printf ' کامیتِ فعلی: %s\n' "$(git rev-parse --short HEAD) $(git log -1 --format=%s)"
# ─── ۲. محیطِ پایتون ────────────────────────────────────────────────────────
step "فعال‌سازیِ محیطِ مجازی"
if [ -d venv ]; then
# shellcheck disable=SC1091
source venv/bin/activate
printf ' venv فعال شد\n'
elif [ -n "${VIRTUAL_ENV:-}" ]; then
printf ' از venv فعالِ فعلی استفاده می‌شود: %s\n' "$VIRTUAL_ENV"
else
warn "هیچ venv ای پیدا نشد؛ از پایتونِ سیستم استفاده می‌شود."
fi
# ─── ۳. وابستگی‌ها ──────────────────────────────────────────────────────────
# همیشه اجرا می‌شود، نه فقط وقتی requirements عوض شده: تشخیصِ «عوض شده یا نه»
# خودش یک جای فراموش‌شدنیِ دیگر است، و pip وقتی همه‌چیز نصب است سریع تمام می‌کند.
step "نصبِ وابستگی‌ها"
PIP_ARGS=(-r requirements.txt --disable-pip-version-check)
if [ -n "${PIP_INDEX_URL:-}" ]; then
printf ' از ایندکسِ %s استفاده می‌شود\n' "$PIP_INDEX_URL"
fi
python -m pip install "${PIP_ARGS[@]}" \
|| fail "نصبِ وابستگی‌ها شکست خورد. اگر pypi.org تایم‌اوت می‌دهد، این را امتحان کنید:
PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple ./deploy.sh"
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
step "اعمالِ مهاجرت‌های دیتابیس"
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ری‌استارت نشد."
# ─── ۵. فایل‌های استاتیک ───────────────────────────────────────────────────
# فقط در حالتِ پروداکشن لازم است: با DEBUG=1 خودِ whitenoise از finders می‌خواند.
step "جمع‌آوریِ فایل‌های استاتیک"
if python -c "
import django, os
os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings')
django.setup()
from django.conf import settings
raise SystemExit(0 if settings.DEBUG else 1)
" 2>/dev/null; then
printf ' DEBUG روشن است — لازم نیست.\n'
else
python manage.py collectstatic --noinput >/dev/null \
|| warn "collectstatic شکست خورد؛ پنلِ ادمین ممکن است بدونِ CSS بالا بیاید."
printf ' انجام شد.\n'
fi
# ─── ۶. بررسی‌های پیش از راه‌اندازی ────────────────────────────────────────
step "بررسیِ تنظیماتِ استقرار"
python manage.py check --deploy 2>&1 | sed 's/^/ /' || true
# ─── ۷. ری‌استارت ──────────────────────────────────────────────────────────
step "ری‌استارتِ سرویس"
SERVICE="${TADRISINO_SERVICE:-}"
if [ -z "$SERVICE" ] && command -v systemctl >/dev/null 2>&1; then
for candidate in tadrisino tadrisino-api mathapp gunicorn; do
if systemctl list-unit-files "${candidate}.service" >/dev/null 2>&1 \
&& systemctl cat "${candidate}.service" >/dev/null 2>&1; then
SERVICE="$candidate"; break
fi
done
fi
if [ -n "$SERVICE" ]; then
sudo systemctl restart "$SERVICE" && printf ' سرویسِ %s ری‌استارت شد\n' "$SERVICE"
else
warn "سرویسِ systemd پیدا نشد؛ خودتان ری‌استارت کنید، مثلاً:
sudo systemctl restart <نامِ سرویس>
و برای اینکه دفعه‌ی بعد خودکار شود:
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh"
fi
# ─── ۸. آیا واقعاً بالا آمد؟ ───────────────────────────────────────────────
step "بررسیِ سلامت"
printf ' %s\n' "$HEALTH_URL"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
BODY="$(curl -s -m 10 "$HEALTH_URL" 2>/dev/null || true)"
CODE="$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo 000)"
if [ "$CODE" = "200" ]; then
printf '%s✓ سرویس سالم بالا آمد.%s\n' "$C_OK" "$C_OFF"
printf '%s\n' "$BODY" | python -m json.tool 2>/dev/null | sed 's/^/ /' || printf ' %s\n' "$BODY"
exit 0
fi
sleep 3
done
printf '%s✖ سرویس بعد از ۳۰ ثانیه هنوز سالم نیست (آخرین کد: %s)%s\n' "$C_ERR" "$CODE" "$C_OFF" >&2
printf '%s\n' "$BODY" | head -c 600 >&2; echo >&2
cat >&2 <<'HINT'
لاگ را ببینید:
sudo journalctl -u <نامِ سرویس> -n 60 --no-pager
رایج‌ترین علت‌ها:
ModuleNotFoundError → وابستگیِ جدید نصب نشده (این اسکریپت باید حلش می‌کرد)
ImproperlyConfigured → متغیرِ محیطیِ لازم ست نیست (SECRET_KEY / CACHE_URL)
502 از nginx → پروسه‌ی جنگو اصلاً بالا نیامده
HINT
exit 1