Files
MathApp/deploy.sh
Zareian eaa4b761dd stop a deploy from silently blocking the frontend or leaving 0000 open
سه حالتِ خراب که هیچ‌کدام در لاگِ سرور ردی نمی‌گذارند و فقط وقتی کشف می‌شدند
که یک آدم از بیرون اپ را دستی امتحان کند.

CORS_ALLOWED_ORIGINS فقط از env خوانده می‌شد و با DEBUG روشن به localhost
محدود می‌ماند. یعنی بک‌اندِ کاملاً سالم، ولی مرورگر همه‌ی درخواست‌های
math.tadrisino2.ir را بلاک می‌کند — خطایی که سمتِ سرور دیده نمی‌شود و فقط در
کنسولِ مرورگر پیداست. origin های شناخته‌شده حالا پیش‌فرضِ داخلِ کدند تا یک
متغیرِ فراموش‌شده اتصال را نشکند؛ env همچنان بازنویسی‌شان می‌کند.

/api/health حالا warnings هم می‌دهد. مهم‌ترینش: با DEBUG روشن روی سرورِ عمومی،
DEV_MASTER_OTP یعنی هر کسی با کدِ 0000 می‌تواند به‌جای هر شماره‌ای وارد شود.
این تا امروز فقط با خواندنِ تنظیماتِ سرور معلوم می‌شد، و ما به سرور دسترسی
نداریم. allowedOrigins هم برگردانده می‌شود تا مشکلِ CORS از بیرون قابلِ
تشخیص باشد.

deploy.sh قبل از دست زدن به دیتابیس متغیرهای لازم را بررسی می‌کند و با پیامِ
روشن می‌ایستد، به‌جای اینکه وسطِ کار بشکند و 502 بدهد.
.env.production.example همان‌ها را یک‌جا مستند می‌کند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:46:23 +03:30

170 lines
9.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
# ─── ۳.۵ بررسیِ متغیرهای محیطی، قبل از دست زدن به دیتابیس ──────────────────
# چرا اینجا: اگر SECRET_KEY یا CACHE_URL نباشد، جنگو موقعِ بالا آمدن می‌میرد و
# از بیرون فقط 502 دیده می‌شود. بهتر است همین‌جا و با پیامِ روشن متوقف شویم.
step "بررسیِ متغیرهای محیطی"
python - <<'PYCHECK' || fail "متغیرهای محیطیِ لازم ست نیستند؛ هیچ تغییری اعمال نشد."
import os, sys
debug = os.environ.get('DJANGO_DEBUG', '').strip().lower() in {'1', 'true', 'yes', 'on'}
missing = []
if not os.environ.get('DJANGO_ALLOWED_HOSTS', '').strip():
missing.append(('DJANGO_ALLOWED_HOSTS', 'دامنه‌هایی که سرو می‌شوند، مثلاً api.math.tadrisino2.ir'))
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.production.example).')
sys.exit(1)
if debug:
print(' ⚠ DJANGO_DEBUG روشن است.')
print(' روی سرورِ عمومی یعنی: صفحه‌های خطا کلِ تنظیمات را نشان می‌دهند،')
print(' و کدِ ورودِ 0000 برای هر شماره‌ای پذیرفته می‌شود.')
else:
print(' ✓ حالتِ پروداکشن')
PYCHECK
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
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