diff --git a/CLAUDE.md b/CLAUDE.md index f80d091..979bc7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -830,11 +830,12 @@ migrationها اعمال شدند، سه‌یار و `nextLessonId` از بیر بی‌بکاپ migrate نمی‌شود)؛ روی سرور هم اجرا شد (۲۰۲۶-۱۰-۰۷، `integrity_check: ok`). بیرون از ماشین بکاپی نیست. -**⚠️ دیپلو از ۲۰۲۶-۱۰-۰۷ به‌خاطر Gitea:** از روی خودِ سرور `https://gitea.tadrisino2.ir` (حل‌شده به IPهای -ArvanCloud) ۵۰۴ می‌دهد، پس `deploy.sh` در `git fetch` می‌ایستد. آن روز کد با `git bundle` (از لپ‌تاپ، -`scp` به `.deploy-backups/`، `git fetch main:refs/remotes/origin/main`، هشِ کامیت مقایسه شد، -`git reset --hard origin/main`، `SKIP_PULL=1 ./deploy.sh`) رسید. علتِ ۵۰۴ پیدا نشد (مسیرِ آروان↔سرور؛ از لپ‌تاپ ۲۰۰)؛ -مرحله‌ی «بررسیِ سلامت»ِ `deploy.sh` هم از داخلِ سرور همیشه شکست می‌خورد (همان مسیر). سلامت را از بیرون بسنجید. `sqlite3` روی سرور نصب نیست — برای بکاپِ دستی با `venv/bin/python` و +**⚠️ Gitea از روی سرور:** از ۲۰۲۶-۱۰-۰۷ `https://gitea.tadrisino2.ir` از خودِ سرور (حل‌شده به IPهای +ArvanCloud) ۵۰۴ می‌دهد و از لپ‌تاپ ۲۰۰؛ علتش پیدا نشد (مسیرِ آروان↔سرور). `deploy.sh` در `git fetch` می‌ایستد. +راه: `DEPLOY_BUNDLE` (بخشِ «دیپلو» در `docs/operations.md`)، با مقایسه‌ی هشِ کامیت. مرحله‌ی «بررسیِ سلامت» +حالا از سوکتِ gunicorn می‌سنجد و از داخلِ سرور هم کار می‌کند. + +`sqlite3` روی سرور نصب نیست — برای بکاپِ دستی با `venv/bin/python` و `sqlite3.Connection.backup()` بگیر، نه با `cp`. بخش‌های تاریخیِ زیر به‌ترتیبِ زمان نگه داشته شده‌اند چون درس‌هایشان هنوز معتبر است؛ diff --git a/deploy.sh b/deploy.sh index 56bf580..ff5fba2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -39,6 +39,21 @@ else ) fi +# یک آدرسِ سلامت را می‌زند و CODE و BODY را می‌گذارد. آدرسِ `unix:/مسیر` یعنی مستقیم از سوکتِ +# gunicorn (با هدرِ Host دامنه‌ی خودمان): از داخلِ سرور آدرسِ عمومی از مسیرِ ArvanCloud برمی‌گردد و +# ۲۰۲۶-۱۰-۰۷ ۵۰۴/۰۰۰ می‌داد، پس «سلامتِ» اسکریپت همیشه شکست می‌خورد حتی وقتی سرویس سالم بود. +probe_health() { + local target="$1" RESPONSE + if [[ "$target" == unix:* ]]; then + RESPONSE="$(curl -s -m 10 -w '\n%{http_code}' --unix-socket "${target#unix:}" \ + -H "Host: ${PUBLIC_HOST:-localhost}" http://localhost/api/health 2>/dev/null || printf '\n000')" + else + RESPONSE="$(curl -s -m 10 -w '\n%{http_code}' "$target" 2>/dev/null || printf '\n000')" + fi + CODE="${RESPONSE##*$'\n'}" + BODY="${RESPONSE%$'\n'*}" +} + # ─── ۱. کدِ تازه ──────────────────────────────────────────────────────────── if [ "${SKIP_PULL:-0}" != "1" ]; then step "گرفتنِ کدِ تازه" @@ -49,8 +64,21 @@ if [ "${SKIP_PULL:-0}" != "1" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)" [ "$BRANCH" = "HEAD" ] && BRANCH=main - git fetch origin "$BRANCH" \ - || fail "به gitea وصل نشد. شبکه یا دسترسیِ مخزن را بررسی کنید." + # DEPLOY_BUNDLE=/مسیر/فایل.bundle: کد را از یک git bundle می‌گیرد، نه از gitea. فقط وقتی صریحاً + # داده شود؛ برای روزی که سرور به gitea نمی‌رسد (۲۰۲۶-۱۰-۰۷: از سرور ۵۰۴ از مسیرِ ArvanCloud، از بیرون + # ۲۰۰). bundle را از جایی بسازید که به origin/ دسترسی دارد و هشِ کامیتِ چاپ‌شده را با آنجا + # مقایسه کنید. بدونِ این متغیر هیچ جایگزینی نیست و نرسیدن به gitea همچنان دیپلو را می‌ایستاند. + if [ -n "${DEPLOY_BUNDLE:-}" ]; then + [ -f "$DEPLOY_BUNDLE" ] || fail "DEPLOY_BUNDLE=$DEPLOY_BUNDLE وجود ندارد." + warn "کد از bundle گرفته می‌شود، نه از gitea: $DEPLOY_BUNDLE" + git bundle verify "$DEPLOY_BUNDLE" >/dev/null 2>&1 || fail "bundle معتبر نیست یا کامیتِ پایه‌اش روی این سرور نیست." + git fetch "$DEPLOY_BUNDLE" "$BRANCH:refs/remotes/origin/$BRANCH" \ + || fail "گرفتن از bundle نشد." + else + git fetch origin "$BRANCH" \ + || fail "به gitea وصل نشد. شبکه یا دسترسیِ مخزن را بررسی کنید (اگر فقط از این سرور نمی‌رسد: DEPLOY_BUNDLE، بالای همین فایل)." + fi + printf ' origin/%s = %s\n' "$BRANCH" "$(git rev-parse --short "origin/$BRANCH")" # اگر کسی روی سرور فایلی را دستی عوض کرده، اول نسخه‌ی پشتیبان، بعد کنار # گذاشتنش. @@ -350,26 +378,31 @@ fi # ─── ۸. آیا واقعاً بالا آمد؟ ─────────────────────────────────────────────── step "بررسیِ سلامت" +PUBLIC_URL="" if [ -z "${TADRISINO_HEALTH_URL:-}" ]; then PUBLIC_URL="$(python -c 'import django, os; os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings"); django.setup(); from django.conf import settings; print(settings.PUBLIC_BASE_URL)' 2>/dev/null | tail -n 1)" if [ -n "$PUBLIC_URL" ]; then + PUBLIC_HOST="${PUBLIC_URL#*://}"; PUBLIC_HOST="${PUBLIC_HOST%%/*}" + # سوکتِ gunicorn را از خودِ یونیت می‌خوانیم تا مسیرش در این فایل تکرار نشود. + SOCK="" + if [ -n "${SERVICE:-}" ] && command -v systemctl >/dev/null 2>&1; then + SOCK="$(systemctl cat "${SERVICE}.service" 2>/dev/null | grep -oE 'unix:[^ "]+' | head -n 1 || true)" + fi + [ -n "$SOCK" ] && [ -S "${SOCK#unix:}" ] && HEALTH_URLS=("$SOCK" "${HEALTH_URLS[@]}") HEALTH_URLS+=("$PUBLIC_URL/api/health" "$PUBLIC_URL/api/health/") fi fi CODE=000; BODY="" for attempt in 1 2 3 4 5 6 7 8 9 10; do for url in "${HEALTH_URLS[@]}"; do - # یک درخواست، نه دو تا: دو فراخوانیِ جدا می‌توانست دو وضعیتِ متفاوت ببیند. - RESPONSE="$(curl -s -m 10 -w '\n%{http_code}' "$url" 2>/dev/null || printf '\n000')" - CODE="${RESPONSE##*$'\n'}" - BODY="${RESPONSE%$'\n'*}" + probe_health "$url" 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 diff --git a/docs/operations.md b/docs/operations.md index 83376b6..309de12 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -34,7 +34,15 @@ poll کردنِ `/api/health`. > `systemctl show zareian-mathapp -p ActiveEnterTimestamp` متغیرهای اختیاری: `SKIP_PULL=1`، `TADRISINO_SERVICE`، `TADRISINO_HEALTH_URL`، -`PIP_INDEX_URL` (وقتی pypi.org تایم‌اوت می‌دهد). +`PIP_INDEX_URL` (وقتی pypi.org تایم‌اوت می‌دهد)، `DEPLOY_BUNDLE`. + +- **سلامت از سوکتِ gunicorn** (مسیرش را از یونیت می‌خواند) با هدرِ `Host` دامنه‌ی خودمان سنجیده + می‌شود، چون از داخلِ سرور آدرسِ عمومی از مسیرِ ArvanCloud برمی‌گردد و ۵۰۴/۰۰۰ می‌داد (۲۰۲۶-۱۰-۰۷). +- **اگر سرور به Gitea نمی‌رسد** (از سرور ۵۰۴، از لپ‌تاپ ۲۰۰): روی لپ‌تاپ + `git bundle create x.bundle <هشِ origin/main روی سرور>..main`، `scp` به + `~/mathapp-v2/.deploy-backups/`، و روی سرور + `DEPLOY_BUNDLE=.deploy-backups/x.bundle ./deploy.sh`. هشِ `origin/main` که چاپ می‌شود را با لپ‌تاپ + مقایسه کنید. بدونِ این متغیر جایگزینی نیست و نرسیدن به Gitea دیپلو را می‌ایستاند. ## بررسیِ سلامت