#!/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