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:
11
CLAUDE.md
11
CLAUDE.md
@@ -830,11 +830,12 @@ migrationها اعمال شدند، سهیار و `nextLessonId` از بیر
|
|||||||
بیبکاپ migrate نمیشود)؛ روی سرور هم اجرا شد (۲۰۲۶-۱۰-۰۷، `integrity_check: ok`). بیرون از ماشین بکاپی
|
بیبکاپ migrate نمیشود)؛ روی سرور هم اجرا شد (۲۰۲۶-۱۰-۰۷، `integrity_check: ok`). بیرون از ماشین بکاپی
|
||||||
نیست.
|
نیست.
|
||||||
|
|
||||||
**⚠️ دیپلو از ۲۰۲۶-۱۰-۰۷ بهخاطر Gitea:** از روی خودِ سرور `https://gitea.tadrisino2.ir` (حلشده به IPهای
|
**⚠️ Gitea از روی سرور:** از ۲۰۲۶-۱۰-۰۷ `https://gitea.tadrisino2.ir` از خودِ سرور (حلشده به IPهای
|
||||||
ArvanCloud) ۵۰۴ میدهد، پس `deploy.sh` در `git fetch` میایستد. آن روز کد با `git bundle` (از لپتاپ،
|
ArvanCloud) ۵۰۴ میدهد و از لپتاپ ۲۰۰؛ علتش پیدا نشد (مسیرِ آروان↔سرور). `deploy.sh` در `git fetch` میایستد.
|
||||||
`scp` به `.deploy-backups/`، `git fetch <bundle> main:refs/remotes/origin/main`، هشِ کامیت مقایسه شد،
|
راه: `DEPLOY_BUNDLE` (بخشِ «دیپلو» در `docs/operations.md`)، با مقایسهی هشِ کامیت. مرحلهی «بررسیِ سلامت»
|
||||||
`git reset --hard origin/main`، `SKIP_PULL=1 ./deploy.sh`) رسید. علتِ ۵۰۴ پیدا نشد (مسیرِ آروان↔سرور؛ از لپتاپ ۲۰۰)؛
|
حالا از سوکتِ gunicorn میسنجد و از داخلِ سرور هم کار میکند.
|
||||||
مرحلهی «بررسیِ سلامت»ِ `deploy.sh` هم از داخلِ سرور همیشه شکست میخورد (همان مسیر). سلامت را از بیرون بسنجید. `sqlite3` روی سرور نصب نیست — برای بکاپِ دستی با `venv/bin/python` و
|
|
||||||
|
`sqlite3` روی سرور نصب نیست — برای بکاپِ دستی با `venv/bin/python` و
|
||||||
`sqlite3.Connection.backup()` بگیر، نه با `cp`.
|
`sqlite3.Connection.backup()` بگیر، نه با `cp`.
|
||||||
|
|
||||||
بخشهای تاریخیِ زیر بهترتیبِ زمان نگه داشته شدهاند چون درسهایشان هنوز معتبر است؛
|
بخشهای تاریخیِ زیر بهترتیبِ زمان نگه داشته شدهاند چون درسهایشان هنوز معتبر است؛
|
||||||
|
|||||||
45
deploy.sh
45
deploy.sh
@@ -39,6 +39,21 @@ else
|
|||||||
)
|
)
|
||||||
fi
|
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
|
if [ "${SKIP_PULL:-0}" != "1" ]; then
|
||||||
step "گرفتنِ کدِ تازه"
|
step "گرفتنِ کدِ تازه"
|
||||||
@@ -49,8 +64,21 @@ if [ "${SKIP_PULL:-0}" != "1" ]; then
|
|||||||
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
|
||||||
[ "$BRANCH" = "HEAD" ] && BRANCH=main
|
[ "$BRANCH" = "HEAD" ] && BRANCH=main
|
||||||
|
|
||||||
|
# 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" \
|
git fetch origin "$BRANCH" \
|
||||||
|| fail "به gitea وصل نشد. شبکه یا دسترسیِ مخزن را بررسی کنید."
|
|| fail "به gitea وصل نشد. شبکه یا دسترسیِ مخزن را بررسی کنید (اگر فقط از این سرور نمیرسد: DEPLOY_BUNDLE، بالای همین فایل)."
|
||||||
|
fi
|
||||||
|
printf ' origin/%s = %s\n' "$BRANCH" "$(git rev-parse --short "origin/$BRANCH")"
|
||||||
|
|
||||||
# اگر کسی روی سرور فایلی را دستی عوض کرده، اول نسخهی پشتیبان، بعد کنار
|
# اگر کسی روی سرور فایلی را دستی عوض کرده، اول نسخهی پشتیبان، بعد کنار
|
||||||
# گذاشتنش.
|
# گذاشتنش.
|
||||||
@@ -350,26 +378,31 @@ fi
|
|||||||
|
|
||||||
# ─── ۸. آیا واقعاً بالا آمد؟ ───────────────────────────────────────────────
|
# ─── ۸. آیا واقعاً بالا آمد؟ ───────────────────────────────────────────────
|
||||||
step "بررسیِ سلامت"
|
step "بررسیِ سلامت"
|
||||||
|
PUBLIC_URL=""
|
||||||
if [ -z "${TADRISINO_HEALTH_URL:-}" ]; then
|
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)"
|
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
|
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/")
|
HEALTH_URLS+=("$PUBLIC_URL/api/health" "$PUBLIC_URL/api/health/")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
CODE=000; BODY=""
|
CODE=000; BODY=""
|
||||||
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
for url in "${HEALTH_URLS[@]}"; do
|
for url in "${HEALTH_URLS[@]}"; do
|
||||||
# یک درخواست، نه دو تا: دو فراخوانیِ جدا میتوانست دو وضعیتِ متفاوت ببیند.
|
probe_health "$url"
|
||||||
RESPONSE="$(curl -s -m 10 -w '\n%{http_code}' "$url" 2>/dev/null || printf '\n000')"
|
|
||||||
CODE="${RESPONSE##*$'\n'}"
|
|
||||||
BODY="${RESPONSE%$'\n'*}"
|
|
||||||
if [ "$CODE" = "200" ]; then
|
if [ "$CODE" = "200" ]; then
|
||||||
printf '%s✓ سرویس سالم بالا آمد%s (%s)\n' "$C_OK" "$C_OFF" "$url"
|
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"
|
printf '%s\n' "$BODY" | python -m json.tool 2>/dev/null | sed 's/^/ /' || printf ' %s\n' "$BODY"
|
||||||
# ۲۰۰ گرفتن بهتنهایی یعنی «چیزی جواب میدهد»، نه «کدِ جدید بالاست».
|
# ۲۰۰ گرفتن بهتنهایی یعنی «چیزی جواب میدهد»، نه «کدِ جدید بالاست».
|
||||||
if [ "$RESTART_OK" != "1" ]; then
|
if [ "$RESTART_OK" != "1" ]; then
|
||||||
warn "ولی سرویس ریاستارت نشد — این جوابْ از پروسهی قدیمی است و کدِ
|
warn "ولی سرویس ریاستارت نشد — این جوابْ از پروسهی قدیمی است و کدِ
|
||||||
جدید هنوز سرو نمیشود. تا ریاستارت نکنید دیپلوی ناتمام است."
|
جدید هنوز سرو نمیشود. تا ریاستارت نکنید دیپلو ناتمام است."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -34,7 +34,15 @@ poll کردنِ `/api/health`.
|
|||||||
> `systemctl show zareian-mathapp -p ActiveEnterTimestamp`
|
> `systemctl show zareian-mathapp -p ActiveEnterTimestamp`
|
||||||
|
|
||||||
متغیرهای اختیاری: `SKIP_PULL=1`، `TADRISINO_SERVICE`، `TADRISINO_HEALTH_URL`،
|
متغیرهای اختیاری: `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 دیپلو را میایستاند.
|
||||||
|
|
||||||
## بررسیِ سلامت
|
## بررسیِ سلامت
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user