let a deploy repair the server instead of stopping at it

تا حالا اگر کسی روی سرور فایلی را دستی عوض کرده بود، `git pull --ff-only`
شکست می‌خورد و کلِ اسکریپت همان‌جا می‌ایستاد. چون هیچ‌کدام از مراحلِ بعدی
اجرا نمی‌شد، سرور با کدِ قدیمی می‌ماند — بی‌صدا. یک بار همین دو روز طول کشید.

حالا مرحله‌ی اول fetch می‌کند، اگر تغییرِ دستی (یا کامیتِ محلی) باشد آن را در
.deploy-backups/*.patch نگه می‌دارد، و با `git reset --hard origin/<branch>`
کنارش می‌گذارد و ادامه می‌دهد. سرور در هر وضعیتی باشد، یک اجرا دقیقاً همان
چیزی می‌شود که روی gitea است.

`git clean` عمداً زده نمی‌شود و نباید اضافه شود: db.sqlite3 و .env و
.django_secret_key ردیابی‌نشده و داخلِ همین پوشه‌اند.

با مخزنِ ساختگی هر سه حالت آزموده شد: تغییرِ دستی، درختِ تمیز، و کامیتِ محلیِ
واگرا. در هر سه، .env و db.sqlite3 دست‌نخورده ماندند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 00:54:06 +03:30
parent 6ec82081b5
commit 8eedbdeac0
3 changed files with 67 additions and 4 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ data/textbooks/*
# خروجیِ collectstatic — تولیدی است، در هر دیپلوی از نو ساخته می‌شود. # خروجیِ collectstatic — تولیدی است، در هر دیپلوی از نو ساخته می‌شود.
staticfiles/ staticfiles/
# نسخه‌ی پشتیبانِ تغییرهای دستیِ روی سرور که deploy.sh کنار می‌گذارد.
.deploy-backups/

View File

@@ -127,8 +127,38 @@ colourless.
## Operations ## Operations
`./deploy.sh` is the whole release: pull, install, migrate, collectstatic, restart, then **The whole release, as one line:**
it polls `/api/health` and prints the likely cause if the service does not come back.
```bash
ssh -t administrator@78.38.35.249 'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp'
```
It asks for the password twice — once for ssh, once for sudo. **Wait for each prompt**;
pasting several lines at once feeds the queued text to the password prompt and locks you
out for three tries.
The trailing restart is separate because `sudo` inside `deploy.sh` cannot prompt for a
password. The script no longer pretends that worked: it compares the service's start time
before and after and exits non-zero when the restart did not happen, so a half-finished
deploy can never again look like a successful one. Giving `administrator` a `NOPASSWD`
sudoers line for just `systemctl restart zareian-mathapp` would collapse this to a single
command; that is the server admin's call, not ours.
`./deploy.sh` is the whole release: fetch, reset to `origin/<branch>`, install, migrate,
seed content, collectstatic, restart, then it polls `/api/health` and prints the likely
cause if the service does not come back.
**It repairs the server rather than stopping at it.** A hand-edited file used to make
`git pull --ff-only` fail, which aborted the whole script — and because every later step
was skipped, the server silently stayed on old code. Now local modifications (and local
commits) are saved to a timestamped patch under `.deploy-backups/` and then discarded with
`git reset --hard origin/<branch>`, and the deploy continues. Whatever state the server is
left in, one run puts it back on exactly what Gitea has.
**`git clean` is never run there and must never be added.** `db.sqlite3`, `.env` and
`.django_secret_key` are untracked and live in that directory; `reset --hard` leaves them
alone, `clean -fd` would destroy the database, the config and every signed-in session.
Settings boot with working defaults (hosts, CSRF, CORS, secret key, cache), so a stale or Settings boot with working defaults (hosts, CSRF, CORS, secret key, cache), so a stale or
partial `.env` on the server no longer breaks startup — env values are *merged with* partial `.env` on the server no longer breaks startup — env values are *merged with*
those defaults, not replaced by them. those defaults, not replaced by them.

View File

@@ -42,9 +42,39 @@ fi
# ─── ۱. کدِ تازه ──────────────────────────────────────────────────────────── # ─── ۱. کدِ تازه ────────────────────────────────────────────────────────────
if [ "${SKIP_PULL:-0}" != "1" ]; then if [ "${SKIP_PULL:-0}" != "1" ]; then
step "گرفتنِ کدِ تازه" step "گرفتنِ کدِ تازه"
# امضای خودِ این فایل قبل از pull. توضیحش پایین‌تر. # امضای خودِ این فایل قبل از به‌روزرسانی. توضیحش پایین‌تر.
SELF_BEFORE="$(cksum "$0" 2>/dev/null || true)" SELF_BEFORE="$(cksum "$0" 2>/dev/null || true)"
git pull --ff-only || fail "git pull نشد. اگر تغییرِ محلی دارید اول تعیین تکلیفش کنید."
# شاخه‌ی فعلی؛ اگر HEAD جدا افتاده باشد main مبنا می‌شود.
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
[ "$BRANCH" = "HEAD" ] && BRANCH=main
git fetch origin "$BRANCH" \
|| fail "به gitea وصل نشد. شبکه یا دسترسیِ مخزن را بررسی کنید."
# اگر کسی روی سرور فایلی را دستی عوض کرده، اول نسخه‌ی پشتیبان، بعد کنار
# گذاشتنش.
#
# چرا خودکار و نه توقف: قبلاً اینجا `git pull --ff-only` بود و با هر تغییرِ
# دستی کلِ دیپلوی همان‌جا می‌ایستاد — و چون بقیه‌ی مراحل اجرا نمی‌شد، سرور
# با کدِ قدیمی می‌ماند بدونِ اینکه کسی بفهمد. دقیقاً همین یک بار دو روز طول
# کشید. حالا تغییرِ دستی مانعِ استقرار نیست؛ ذخیره می‌شود و رد می‌شویم.
if ! git diff --quiet HEAD 2>/dev/null; then
mkdir -p .deploy-backups
PATCH=".deploy-backups/local-changes-$(date +%Y%m%d-%H%M%S).patch"
git diff HEAD > "$PATCH" 2>/dev/null || true
warn "روی سرور تغییرِ دستی پیدا شد. در $PATCH ذخیره شد و کنار گذاشته می‌شود.
برای دیدنش: cat $PATCH
اگر لازم بود: git apply $PATCH"
fi
# ⚠️ اینجا عمداً `git clean` زده نمی‌شود و هرگز هم نباید زده شود:
# db.sqlite3 و .env و .django_secret_key هیچ‌کدام در گیت نیستند و داخلِ همین
# پوشه‌اند. یک `git clean -fd` یعنی نابودیِ دیتابیس، تنظیمات، و کلیدی که
# همه‌ی نشست‌های ورود با آن امضا شده‌اند. reset --hard فقط فایل‌های ردیابی‌شده
# را برمی‌گرداند و به این‌ها کار ندارد.
git reset --hard "origin/$BRANCH" \
|| fail "برگرداندن به origin/$BRANCH نشد؛ هیچ تغییری اعمال نشد."
# اگر همین اسکریپت در pull عوض شد، نسخه‌ی تازه از اول اجرا می‌شود. # اگر همین اسکریپت در pull عوض شد، نسخه‌ی تازه از اول اجرا می‌شود.
# #