bash اسکریپت را یکجا نمیخواند؛ حین اجرا از روی دیسک ادامه میدهد. پس وقتی
`git pull` همین فایل را عوض میکند، ادامهی اجرا مخلوطی از نسخهی قدیم و جدید
است. یک بار همین اتفاق افتاد و گران تمام شد: نسخهی قدیمی که در حال اجرا بود
سرویسِ تیمِ دیگری را ریاستارت کرد، در حالی که نسخهی اصلاحشده همان لحظه روی
دیسک نشسته بود.
حالا امضای فایل قبل و بعدِ pull مقایسه میشود و اگر عوض شده باشد، نسخهی تازه
از اول اجرا میشود (با SKIP_PULL=1 و یک نگهبان در برابرِ حلقهی بیپایان).
و یک حفرهی کوچکترِ همخانواده: اگر زمانِ بالا آمدنِ سرویس اصلاً خوانده نشود،
دیگر موفقیت فرض نمیشود. «نمیدانم» همان چیزی بود که دو روز پنهانش کرد.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
روی این سرور یک `gunicorn.service` هست که مالِ تیمِ دیگری است. هر کسی که
عجله داشته باشد و `systemctl restart gunicorn` بزند، سرویسِ آنها را
میخواباند و سرویسِ ما اصلاً ریاستارت نمیشود — یعنی دیپلوی بیصدا اعمال
نمیشود. همین دیشب یک بار اتفاق افتاد.
حالا همان یونیت با نامِ `zareian-mathapp` هم صدا زده میشود (symlink در
/etc/systemd/system). نامِ قبلی هم کار میکند، پس چیزی نمیشکند.
خط تیره و نه نقطه: systemd آخرین نقطه را جداکنندهی نوعِ یونیت میداند، پس
`zareian.mathapp` را باید هر بار کامل با `.service` نوشت.
deploy.sh هم اول دنبالِ همین نام میگردد.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
سرور دو روز کدِ قدیمی را سرو کرد و هیچکس نفهمید. علتش این بود که
`systemctl restart` شکست خورده بود ولی پروسهی قبلی هنوز بالا بود و همان
۲۰۰ را میداد — پس اسکریپت «✓ سرویس سالم بالا آمد» چاپ کرد و با کدِ صفر
تمام شد. محتوا و فایلهای استاتیک هم روی دیسک بودند و سرو نمیشدند.
حالا زمانِ بالا آمدنِ سرویس قبل و بعد مقایسه میشود. اگر عوض نشده باشد،
۲۰۰ گرفتن دیگر موفقیت حساب نمیشود و اسکریپت با خطا تمام میکند.
نامِ عامِ `gunicorn` هم از فهرستِ حدسِ سرویس برداشته شد: روی سرورِ واقعی یک
`gunicorn.service` هست که مالِ تیمِ دیگری است، و حدس زدنش یعنی ریاستارت
کردنِ سرویسِ یک نفرِ دیگر. نامِ درستِ ما `gunicorn-mathapp` است.
`api/health/` هم کنارِ `api/health` ثبت شد. پایشِ سرور شکلِ اسلشدار را صدا
میزد و چون ثبت نبود، کسی روی سرور `config/urls.py` را دستی ویرایش کرده
بود — و از آن به بعد هر `git pull` روی همان فایل گیر میکرد.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
پنلِ ادمین روی سرور بدونِ CSS بالا آمد و هیچ خطایی هم در لاگ نبود.
علت ترتیبِ کارها بود: این مرحله وقتی DEBUG روشن بود رد میشد، چون در آن حالت
whitenoise از finders میخواند و جمعآوری لازم نیست. کسی با DEBUG روشن دیپلوی
کرد و بعد DEBUG را خاموش کرد — کارِ درستی هم بود — و از آن لحظه جنگو دنبالِ
فایلهایی میگشت که هیچوقت جمع نشده بودند.
یعنی درستیِ دیپلوی به ترتیبِ کارِ یک آدم گره خورده بود. چند ثانیه صرفهجویی،
به قیمتِ شکستی که فقط در مرورگر دیده میشود. حالا بیقید و شرط اجرا میشود؛
idempotent است و در هر دو حالت درست کار میکند.
staticfiles/ هم به gitignore اضافه شد: حالا که این مرحله همیشه اجرا میشود،
روی ماشینِ هر توسعهدهندهای ساخته میشود و ۴٫۴ مگابایت فایلِ تولیدی نباید
با یک `git add .` واردِ مخزن شود.
بررسی شد که با DEBUG=False کلِ مسیر کار میکند: استاتیک ۲۰۰، صفحهی ادمین
۲۰۰، API ۲۰۰، و ورود با OTP از پنل. کدِ 0000 همانطور که باید، رد میشود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
سرورِ production از روزِ اول `{"grades": []}` برگردانده: دیتابیس gitignore است
(درست هم هست) و تنها راهِ پر کردنش اجرای `rag_import_collection` و بعد
`seed_curriculum_from_textbook` روی همان سرور بود — یعنی وابستگی به Qdrant، به
اعتبارِ AI که تمام شده، و به کسی که SSH دارد. نتیجه این شد که هیچکس نتوانست
چیزی را تست کند.
حالا برنامهی درسی در `content/seed/curriculum.json` کامیت میشود (۴ پایه، ۳۱
فصل، ۱۶۸ مبحث، ۱۳۰۰ درس، ۵۲۷ کیلوبایت متن) و `deploy.sh` خودش بالا میآوردش.
سرورِ تازه نه Qdrant میخواهد نه ترمینال.
`db.sqlite3` عمداً کامیت نمیشود: باینری است، دیف نمیشود، و هر دیپلوی محتوای
سرور — و فردا پاسخهای کاربران — را با نسخهی لوکالِ یک نفر جایگزین میکند.
سوالها با مسیرِ عنوان ذخیره میشوند نه با lessonId. این تصمیمِ اصلیِ کامیت
است: لوکالِ ما ۱۳۰۷ درس دارد با idهای ۳ تا ۲۰۰۵ (جای خالی، از پاک شدنِ دیتای
دمو)، و همان کوریکولوم روی دیتابیسِ خالی idهای ۱ تا ۱۳۰۷ میگیرد. با id،
سوالِ درسِ ۳ روی سرور به درسِ بیربطی میچسبید — بدونِ هیچ خطایی. تست
`test_question_follows_the_title_path_not_the_id` همین را نگه میدارد.
`seed_content` ایدمپوتنت است و هیچچیز پاک نمیکند: کوریکولوم با عنوان
همسانسازی میشود و سوالِ تکراری رد میشود (`skip_existing`). روی سروری که
کاربرِ واقعی دارد، دیپلوی نباید بتواند محتوا یا پاسخی را از بین ببرد — و
ادمین باید بتواند سوالِ سیدشده را اصلاح کند بیآنکه دیپلویِ بعدی برش گرداند.
ردیفهای `[DEMO]`/`[SEED]` موقعِ export کنار گذاشته میشوند؛ آنها همانهاییاند
که `purge_demo_data` برای پاک کردنشان نوشته شد و جایشان در فایلی که مستقیم روی
production لود میشود نیست.
هنوز صفر سوالِ واقعی داریم: هر ۱۴ سوالِ لوکال رویِ ردیفهای دمو بودند. مسیرِ
سوالها ساخته و تست شده، ولی تا وقتی اعتبارِ AI شارژ نشود فایلش خالی میماند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
دو حالتی که فقط روی سرورِ واقعی پیش میآیند، نه اینجا.
اسکریپتهای قدیمیِ activate به PS1 بدونِ محافظ دست میزنند و زیرِ set -u با
«unbound variable» کلِ دیپلوی را میخوابانند — venv اینجا سالم است ولی venv
سرور لزوماً نه، پس set -u دورِ همان یک خط موقتاً خاموش میشود.
بررسیِ سلامت فقط 127.0.0.1:8000 را میزد. اگر gunicorn روی سوکت یا پورتِ
دیگری باشد، دیپلویِ کاملاً موفق هم با پیامِ خطا تمام میشد. حالا آدرسِ
عمومی هم امتحان میشود و در پایان میگوید کدام آدرسها را زده.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
بازبینیِ کارِ امروز، سه مورد پیدا کرد.
/api/health وقتی مهاجرتِ اعمالنشده وجود داشت میشکست: کدِ فهرستکردنِ
مهاجرتها به migration.key تکیه میکرد که اصلاً وجود ندارد (فیلدها app_label
و name هستند). یعنی دقیقاً در همان حالتی که این بررسی برایش ساخته شده،
بهجای نامِ مهاجرتها فقط AttributeError برمیگرداند. حالا تستی دارد که
مهاجرتِ معلق را شبیهسازی میکند — چیزی که نبودش باعث شد اولین بار غلط بماند.
وارد کردنِ سوال با replace عددِ غلط گزارش میکرد: delete() در جنگو مجموعِ
همهی ردیفهای آبشاری را میدهد، پس «۵ سوال پاک شد» در واقع گزینهها و
پاسخها را هم میشمرد.
deploy.sh بررسیِ متغیرهای محیطیاش با تنظیماتِ جدید ناسازگار شده بود:
متغیرهایی را اجباری میدانست که حالا پیشفرض دارند، و فقط شل را میخواند نه
.env — یعنی روی سرورِ درست هم دیپلوی را رد میکرد. حالا بهجای تکرارِ منطقِ
تنظیمات، از خودِ جنگو میپرسد چه چیزی مؤثر است و گزارش میدهد.
کوچکتر: بررسیِ سلامت یک درخواست میزند نه دو تا، و شکستِ ریاستارت دیگر
اسکریپت را بیصدا تمام نمیکند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
دو فایلِ نمونه یعنی دو جا که باید همزمان بهروز بمانند، و همان چیزی بود که
قبلاً باعث شد نمونه عقب بماند و کسی که سرور را بالا میآورد نداند چه بنویسد.
حالا یک فایل، هم برای ماشینِ محلی هم برای سرور.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
سه حالتِ خراب که هیچکدام در لاگِ سرور ردی نمیگذارند و فقط وقتی کشف میشدند
که یک آدم از بیرون اپ را دستی امتحان کند.
CORS_ALLOWED_ORIGINS فقط از env خوانده میشد و با DEBUG روشن به localhost
محدود میماند. یعنی بکاندِ کاملاً سالم، ولی مرورگر همهی درخواستهای
math.tadrisino2.ir را بلاک میکند — خطایی که سمتِ سرور دیده نمیشود و فقط در
کنسولِ مرورگر پیداست. origin های شناختهشده حالا پیشفرضِ داخلِ کدند تا یک
متغیرِ فراموششده اتصال را نشکند؛ env همچنان بازنویسیشان میکند.
/api/health حالا warnings هم میدهد. مهمترینش: با DEBUG روشن روی سرورِ عمومی،
DEV_MASTER_OTP یعنی هر کسی با کدِ 0000 میتواند بهجای هر شمارهای وارد شود.
این تا امروز فقط با خواندنِ تنظیماتِ سرور معلوم میشد، و ما به سرور دسترسی
نداریم. allowedOrigins هم برگردانده میشود تا مشکلِ CORS از بیرون قابلِ
تشخیص باشد.
deploy.sh قبل از دست زدن به دیتابیس متغیرهای لازم را بررسی میکند و با پیامِ
روشن میایستد، بهجای اینکه وسطِ کار بشکند و 502 بدهد.
.env.production.example همانها را یکجا مستند میکند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
بالا آوردنِ نسخهی جدید تا الان چند کارِ دستی به ترتیبِ درست بود (pull، نصبِ
وابستگیها، مهاجرت، استاتیک، ریاستارت) و فراموش شدنِ هر کدام اپ را میخواباند
با خطایی که از بیرون فقط «502 Bad Gateway» دیده میشد. همین الان هم سرور
دقیقاً به همین دلیل خوابیده: کامیتِ ac1b1af پکیجِ django-cors-headers را اضافه
کرد، و بدونِ pip install جنگو با ModuleNotFoundError بالا نمیآید.
deploy.sh همه را به ترتیب و idempotent انجام میدهد. pip install همیشه اجرا
میشود، نه فقط وقتی requirements عوض شده — تشخیصِ «عوض شده یا نه» خودش یک جای
فراموششدنیِ دیگر است. آخرِ کار خودش سلامت را بررسی میکند و اگر بالا نیامده
باشد علتهای رایج را چاپ میکند، بهجای اینکه کسی بعداً از بیرون کشفش کند.
GET /api/health وضعیت را بدونِ احراز و بدونِ SSH بیرون میدهد: دیتابیس،
مهاجرتهای اعمالنشده، کش، و شمارشِ پایه/درس/سوال روی همان سرور. مهمترینش
مهاجرتِ اعمالنشده است — حالتی که اپ ظاهراً بالا میآید و بعد زیرِ بارِ واقعی
با خطای ستونِ ناموجود میشکند. عمداً هیچ سکرتی برنمیگرداند، فقط بله/خیرِ
«تنظیم شده».
۵۰۳ برمیگرداند وقتی دیتابیس/کش/مهاجرت خراب است تا پایشِ خودکار بتواند رویش
حساب کند؛ نبودِ محتوا خرابی حساب نمیشود ولی گزارش میشود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>