fix two checks that only broke in the case they exist for

بازبینیِ کارِ امروز، سه مورد پیدا کرد.

/api/health وقتی مهاجرتِ اعمال‌نشده وجود داشت می‌شکست: کدِ فهرست‌کردنِ
مهاجرت‌ها به migration.key تکیه می‌کرد که اصلاً وجود ندارد (فیلدها app_label
و name هستند). یعنی دقیقاً در همان حالتی که این بررسی برایش ساخته شده،
به‌جای نامِ مهاجرت‌ها فقط AttributeError برمی‌گرداند. حالا تستی دارد که
مهاجرتِ معلق را شبیه‌سازی می‌کند — چیزی که نبودش باعث شد اولین بار غلط بماند.

وارد کردنِ سوال با replace عددِ غلط گزارش می‌کرد: delete() در جنگو مجموعِ
همه‌ی ردیف‌های آبشاری را می‌دهد، پس «۵ سوال پاک شد» در واقع گزینه‌ها و
پاسخ‌ها را هم می‌شمرد.

deploy.sh بررسیِ متغیرهای محیطی‌اش با تنظیماتِ جدید ناسازگار شده بود:
متغیرهایی را اجباری می‌دانست که حالا پیش‌فرض دارند، و فقط شل را می‌خواند نه
.env — یعنی روی سرورِ درست هم دیپلوی را رد می‌کرد. حالا به‌جای تکرارِ منطقِ
تنظیمات، از خودِ جنگو می‌پرسد چه چیزی مؤثر است و گزارش می‌دهد.

کوچک‌تر: بررسیِ سلامت یک درخواست می‌زند نه دو تا، و شکستِ ری‌استارت دیگر
اسکریپت را بی‌صدا تمام نمی‌کند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 21:42:36 +03:30
parent e5d2427560
commit 3dacf3a50f
4 changed files with 101 additions and 37 deletions

View File

@@ -60,39 +60,40 @@ python -m pip install "${PIP_ARGS[@]}" \
|| fail "نصبِ وابستگی‌ها شکست خورد. اگر pypi.org تایم‌اوت می‌دهد، این را امتحان کنید:
PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple ./deploy.sh"
# ─── ۳.۵ بررسیِ متغیرهای محیطی، قبل از دست زدن به دیتابیس ──────────────────
# چرا اینجا: اگر SECRET_KEY یا CACHE_URL نباشد، جنگو موقعِ بالا آمدن می‌میرد و
# از بیرون فقط 502 دیده می‌شود. بهتر است همین‌جا و با پیامِ روشن متوقف شویم.
step "بررسیِ متغیرهای محیطی"
python - <<'PYCHECK' || fail "متغیرهای محیطیِ لازم ست نیستند؛ هیچ تغییری اعمال نشد."
import os, sys
# ─── ۳.۵ تنظیماتِ مؤثر ─────────────────────────────────────────────────────
# اینجا عمداً چیزی را اجباری نمی‌کنیم: تنظیماتِ پروژه خودش پیش‌فرضِ سالم دارد و
# با فایلِ خالی هم بالا می‌آید. تکرارِ آن منطق در این اسکریپت یعنی دو جا که
# باید هم‌زمان به‌روز بمانند — و همان باعث می‌شد دیپلویِ درست هم رد شود.
# پس فقط از خودِ جنگو می‌پرسیم چه چیزی واقعاً فعال است (شاملِ مقادیرِ .env).
step "تنظیماتِ مؤثر"
python - <<'PYCHECK' || fail "تنظیمات اصلاً بار نشد؛ هیچ تغییری اعمال نشد."
import os
debug = os.environ.get('DJANGO_DEBUG', '').strip().lower() in {'1', 'true', 'yes', 'on'}
missing = []
import django
if not os.environ.get('DJANGO_ALLOWED_HOSTS', '').strip():
missing.append(('DJANGO_ALLOWED_HOSTS', 'دامنه‌هایی که سرو می‌شوند، مثلاً api.math.tadrisino2.ir'))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.conf import settings
if not debug:
if not os.environ.get('DJANGO_SECRET_KEY', '').strip():
missing.append(('DJANGO_SECRET_KEY', 'کلیدِ امضا؛ با DEBUG خاموش اجباری است'))
if not os.environ.get('CACHE_URL', '').strip():
missing.append(('CACHE_URL', 'آدرسِ Redis؛ محدودیتِ نرخ بدونش بینِ workerها مشترک نیست'))
if missing:
print(' این متغیرها ست نیستند:')
for name, why in missing:
print(f' {name:24} {why}')
print()
print(' در فایلِ .env سرور اضافه‌شان کنید (نمونه: .env.example).')
sys.exit(1)
if debug:
print(' ⚠ DJANGO_DEBUG روشن است.')
print(' روی سرورِ عمومی یعنی: صفحه‌های خطا کلِ تنظیمات را نشان می‌دهند،')
print(' و کدِ ورودِ 0000 برای هر شماره‌ای پذیرفته می‌شود.')
print(f' DEBUG : {settings.DEBUG}')
print(f' ALLOWED_HOSTS : {", ".join(settings.ALLOWED_HOSTS) or "—"}')
print(f' CORS مجاز : {", ".join(settings.CORS_ALLOWED_ORIGINS) or "—"}')
print(f' کش : {settings.CACHES["default"]["BACKEND"].rsplit(".", 1)[-1]}')
if os.environ.get('DJANGO_SECRET_KEY'):
key_source = 'از متغیرِ محیطی'
elif settings.DEBUG:
key_source = 'کلیدِ ثابتِ حالتِ توسعه (در پروداکشن استفاده نمی‌شود)'
else:
print(' ✓ حالتِ پروداکشن')
key_source = 'تولیدشده و ذخیره‌شده در .django_secret_key'
print(f' کلیدِ امضا : {key_source}')
if settings.DEBUG:
print()
print(' ⚠ DEBUG روشن است. روی سرورِ عمومی یعنی:')
print(' • صفحه‌های خطا کلِ تنظیمات و traceback را نشان می‌دهند')
print(' • کدِ ورودِ 0000 برای هر شماره‌ای پذیرفته می‌شود')
if 'LocMem' in settings.CACHES['default']['BACKEND'] and not settings.DEBUG:
print(' ⚠ بدونِ Redis محدودیتِ نرخ بینِ workerها مشترک نیست (CACHE_URL).')
PYCHECK
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
@@ -124,16 +125,24 @@ 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
# `systemctl cat` تنها بررسیِ قابلِ اتکاست: list-unit-files حتی وقتی چیزی
# پیدا نکند هم کدِ ۰ برمی‌گرداند.
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
if 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"
# شکستِ ری‌استارت نباید اسکریپت را بی‌صدا تمام کند؛ بررسیِ سلامتِ پایین
# همچنان باید اجرا شود تا معلوم شود سرویسِ قبلی هنوز بالاست یا نه.
if sudo systemctl restart "$SERVICE"; then
printf ' سرویسِ %s ری‌استارت شد\n' "$SERVICE"
else
warn "ری‌استارتِ $SERVICE شکست خورد (شاید sudo رمز می‌خواهد). خودتان اجرا کنید:
sudo systemctl restart $SERVICE"
fi
else
warn "سرویسِ systemd پیدا نشد؛ خودتان ری‌استارت کنید، مثلاً:
sudo systemctl restart <نامِ سرویس>
@@ -145,8 +154,10 @@ 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)"
# یک درخواست، نه دو تا: دو فراخوانیِ جدا می‌توانست دو وضعیتِ متفاوت ببیند.
RESPONSE="$(curl -s -m 10 -w '\n%{http_code}' "$HEALTH_URL" 2>/dev/null || printf '\n000')"
CODE="${RESPONSE##*$'\n'}"
BODY="${RESPONSE%$'\n'*}"
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"