deploy.sh: health check through the gunicorn socket, optional DEPLOY_BUNDLE when Gitea is unreachable

From the server the public URL goes through ArvanCloud and returned 504/000 (2026-10-07), so the
final health step failed even for a healthy service. It now asks the gunicorn socket (path read from
the unit) with our Host header first. DEPLOY_BUNDLE=<file> fetches from a git bundle instead of Gitea,
only when set explicitly; without it an unreachable Gitea still stops the deploy.

Tested: probe_health against a throwaway unix-socket server and a dead URL (bash -n clean).
Not tested: the whole script end to end, until the next deploy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:57:50 +03:30
parent 3e63bd0860
commit 4c601087dc
3 changed files with 55 additions and 13 deletions

View File

@@ -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 <bundle> 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`.
بخش‌های تاریخیِ زیر به‌ترتیبِ زمان نگه داشته شده‌اند چون درس‌هایشان هنوز معتبر است؛

View File

@@ -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/<branch> دسترسی دارد و هشِ کامیتِ چاپ‌شده را با آنجا
# مقایسه کنید. بدونِ این متغیر هیچ جایگزینی نیست و نرسیدن به 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

View File

@@ -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 دیپلو را می‌ایستاند.
## بررسیِ سلامت