نشر المونوريبو
الفهرس
نظرة عامة
المشروع عبارة عن pnpm workspace فيه ثلاثة تطبيقات — client، server، وdb — بيتشاركوا الأدوات ولوك-فايل واحد، لكن كل واحد بيتنشر لوحده: push بيلمس تطبيق واحد بس ميعملش rebuild للتانيين.
النشر نفسه عبر self-hosted GitHub Actions runner + PM2 على VPS بيشغّل CloudPanel — بدون Docker، وبدون منصة إدارة container. القرار مبني على واقع إنتاج فعلي شغّال دلوقتي، مش تصميم نظري — بما فيه حادثة إنتاج حقيقية غيّرت طريقة كتابة متغيّرات البيئة، موثّقة بالتفصيل تحت.
مخطط النشر
جدول التطبيقات
| التطبيق | المسار في الـ workspace | الدور |
|---|---|---|
| client | apps/client | الموقع — Nuxt 4، البورتفوليو والمدونة وصفحات الـ ADR |
| server | apps/server | REST API — Express، مصدر البيانات لكل التطبيقات التانية |
| db | apps/db | لوحة الإدارة — Nuxt 4، لإدارة محتوى الموقع (مشاريع، مقالات، ADRs) |
آلية الديبلوي الانتقائي
الأداة الفعلية اللي بتقرر مين يتبني هي دالة want() جوه scripts/deploy.sh. المنطق بسيط: السكريبت بياخد قايمة المسارات اللي اتغيّرت (عبر git diff بين آخر commit قبل الـ push وبعده)، ولو أي مسار منها بادئته بتطابق مسار تطبيق معيّن، التطبيق ده بيتبني. لو الملف اللي اتغيّر هو pnpm-lock.yaml، أو الديبلوي أول مرة أو شغّال يدوي، التلات تطبيقات بيتبنوا كلهم.
BUILD_ALL=0
if [ "$CHANGED" = "ALL" ] || echo "$CHANGED" | grep -q '^pnpm-lock.yaml$'; then
BUILD_ALL=1
fi
want() { [ "$BUILD_ALL" = 1 ] || echo "$CHANGED" | grep -q "^$1/"; }
if want apps/client; then
# ...build client...
fi الآليتان الحقيقيتان اللي بتحدد النتيجة: فحص git diff للمسارات المتغيّرة، ومطابقة بادئة المسار لكل تطبيق. جدول "ديبلوي موحّد مقابل حسب المسار" تحت في القرارات التصميمية بيشرح ليه الاختيار ده تحديداً، مش بس إزاي بيشتغل.
ما تغيّر أثناء التشغيل — حادثة CRLF
/projects رجّعت 404 في الإنتاج، بينما باقي صفحات الموقع (زي الصفحة الرئيسية) شغّالة عادي — نفس الديبلوي، صفحة واحدة بس واقعة.
أول تشخيص افترض إن الكاش القديم لبناء Nuxt هو السبب — اتصلح بمسح الكاش قبل كل بناء، بس المشكلة فضلت موجودة، وده نفى الفرضية دي فعلياً. السبب الحقيقي كان \r زايد في نهاية قيمة BASE_URL (جايّة من صياغة CRLF في متغيّرات النشر)، وده كان بيكسر بناء الرابط في الطلبات اللي معاها query string بس — و/projects هي الصفحة الوحيدة في الموقع اللي بتنادي useAPI بـ query، فكانت الوحيدة المتأثرة.
الحل كان في مستويين — تنضيف قيمة متغيّرات البيئة من الـ \r قبل ما تتكتب في ملف .env وقت النشر، مع إضافة طبقة حماية دفاعية في useAPI نفسها بتشيل أي مسافة أو محرف زايد من قيمة الـ baseURL قبل ما تُستخدم — عشان أي تلوث مشابه في المستقبل ميكررش نفس المشكلة.
الدرس الأهم مكنش في الإصلاح نفسه، لكن في إن الاعتماد على كتابة صحيحة لمتغيّرات البيئة يدوياً مش كافي — التعقيم لازم يبقى آلي جوه سكريبت النشر نفسه، مش افتراض إن القيمة هتوصل نضيفة دايماً.
القرارات التصميمية
السبب الأساسي: server (Express) محتاج عملية Node دائمة (long-running process)، مش serverless functions — وده وحده كافي يمنع نشره بشكله القياسي على منصة زي Vercel. التكلفة عامل مساعد بس، مش سبب مستقل.
| المعيار | VPS مستضاف ذاتياً | منصة مُدارة (Vercel/Netlify) |
|---|---|---|
| طبيعة server (Express) | عملية Node دائمة — مطلوبة للـ REST API | serverless functions — مش مناسبة لعملية دائمة |
| التقسيم | التلات تطبيقات على نفس البنية | كان محتاج تقسيم على أكتر من منصة |
| التكلفة (عامل مساعد) | VPS واحد ثابت التكلفة لتلات تطبيقات | تكلفة شهرية مضاعفة لتلات تطبيقات منفصلة |
| المعيار | Monorepo واحد | ريبوهات منفصلة |
|---|---|---|
| الأدوات والإعدادات | مشتركة بين التلات تطبيقات | كل ريبو بيكرر إعداداته لوحده |
| الـ lockfile | واحد (pnpm-lock.yaml) لكل الـ dependencies | lockfile منفصل لكل ريبو |
| النشر | مستقل حسب المسار المتغيّر | مستقل بطبيعته (pipeline خاص لكل ريبو) |
| المعيار | ديبلوي موحّد (الكل مع بعض) | ديبلوي حسب المسار (الحالي) |
|---|---|---|
| وقت البناء | بناء التلات تطبيقات في كل push، حتى لو اتغيّر واحد بس | بناء التطبيق المتأثر بس |
| استهلاك الموارد | موارد السيرفر ما بتستحملش 3 عمليات build في نفس الوقت — فحتى لو اتبنوا واحد ورا التاني، وقت النشر بيطول من غير داعي | بناء أقصر — تطبيق واحد بس بيتبني في الحالة الشائعة |
| الآلية | مفيش حاجة تحدد إيه اللي اتغيّر | دالة want() بتقارن مسارات git diff ببادئة كل تطبيق |