DevOps
Drupal تخبرك قبل أن ترقّع. استعمل النافذة.
الـ PSA يحدد اليوم والساعة قبل أن يوجد الإصلاح. ذلك وقت للتحقق من الإصدارات واختبار الـ restore وحجز الموعد، لا وقت لقراءة الـ advisory.
مشكلة السحب والإفلات عبر FTP ليست البطء. المشكلة أن الخادم يصبح مصدر الحقيقة بدل المستودع. تعديل صغير "مؤقت" على ملف مباشرة على الخادم، ثم رفعة تالية من جهازك تمحوه دون أن يلاحظ أحد. ملفّان مختلفان بين جهازك وجهاز زميلك. رفعة انقطعت في منتصفها وتركت الموقع بنصف نسخة. ولا سجلّ يقول من غيّر ماذا ومتى، فحين ينكسر شيء لا يوجد ما تعود إليه.
الهدف من أول خط نشر ليس الأتمتة بحد ذاتها، بل أن تصبح الإجابة على سؤال "ما الذي يعمل على الخادم الآن؟" رقم إيداع (commit) واحداً محدّداً.
أي خط نشر، مهما كبر، هو أربع مراحل:
ابنِها بهذا الترتيب وأضف إليها لاحقاً. خط نشر ينقل الملفات فقط أفضل بمرّات من عدم وجود خط نشر، وأسهل في الفهم من خط مثالي لا تعرف كيف تُصلحه.
rsync مباشرة فوق public_html يعني أن الموقع، لثوانٍ، خليط من نسختين. البديل هو مجلد إصدار لكل نشرة ووصلة رمزية تتحوّل في لحظة واحدة:
~/app/
releases/9f21c4a/ ← نتيجة البناء
releases/3c07e18/ ← الإصدار السابق، محفوظ للتراجع
shared/.env ← أسرار خارج المستودع
shared/uploads/ ← ملفات المستخدمين
current -> releases/9f21c4aوتصبح النشرة نفسها سطراً واحداً: ln -sfn. والتراجع سطراً واحداً أيضاً، وهو نفس السطر مع اسم الإصدار السابق. اجعل public_html وصلة إلى current، أو وجّه جذر المستند إليها إن كانت الاستضافة تسمح.
name: deploy
on:
push:
branches: [main]
# نشرتان متزامنتان تتسابقان على نفس الوصلة الرمزية.
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# ci لا install: يحترم قفل الاعتماديات ويفشل إن اختلّ.
- run: npm ci
- run: npm test
- run: npm run build
- name: تجهيز مفتاح النشر
run: |
install -m 700 -d ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "${{ vars.SSH_PORT }}" "${{ vars.SSH_HOST }}" \
>> ~/.ssh/known_hosts
- name: رفع الإصدار
run: |
rsync -az --delete -e "ssh -p ${{ vars.SSH_PORT }}" \
--exclude '.git' --exclude 'node_modules' \
./ "${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:app/releases/$GITHUB_SHA/"
- name: تفعيل الإصدار
run: |
ssh -p "${{ vars.SSH_PORT }}" "${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}" \
"cd app && ln -sfn releases/$GITHUB_SHA current && ./current/bin/post-deploy"
- name: فحص صحة
run: curl -fsS --retry 5 --retry-delay 3 https://example.com/api/healthssh-keyscan ليس تفصيلاً تجميلياً: بدونه إمّا يتوقف SSH منتظراً تأكيداً لا يأتي، وإمّا تعطّل التحقق من مفتاح المضيف وتقبل أي خادم يجيب.
لا شيء حسّاس داخل المستودع، ولا حتى في مستودع خاص. المتغيّرات الحسّاسة في أسرار المستودع، والملف .env يعيش في shared/ على الخادم ويُوصَل إلى كل إصدار جديد. ولّد مفتاح نشر مخصّصاً لهذا الغرض وحده:
ssh-keygen -t ed25519 -C 'deploy@ci' -f deploy_key -N ''العام في ~/.ssh/authorized_keys على الخادم، والخاص في أسرار المستودع. مفتاح واحد لكل مشروع ولكل بيئة، حتى يكون سحبه رخيصاً.
بعض حسابات الاستضافة المشتركة لا تمنح وصول SSH. الخط نفسه يبقى كما هو، وتتغيّر خطوة النقل وحدها:
lftp -c "open -u $FTP_USER,$FTP_PASS ftps://ftp.example.com; \
mirror -R --delete --parallel=4 \
--exclude .git/ --exclude node_modules/ ./dist /public_html"استخدم ftps لا ftp: البروتوكول العاري يرسل اسم المستخدم وكلمة المرور نصّاً مقروءاً في كل اتصال. واعلم أنّ mirror فوق المجلد الحيّ ليس ذرّياً — يبقى الموقع خليطاً من نسختين لثوانٍ. إن كان الخادم يسمح بإعادة تسمية المجلدات، ارفع إلى مجلد جانبي ثم بدّل الاسمين؛ وإن لم يسمح، فهذه الثواني هي الثمن، واعرفه بدل أن يفاجئك.
نقل الملفات قابل للتراجع؛ حذف عمود ليس كذلك. اجعل كل ترحيل متوافقاً مع الإصدار السابق: أضف العمود الجديد، وانشر الكود الذي يكتب في الاثنين، ثم احذف القديم في نشرة لاحقة. هكذا يبقى التراجع عن نشرة فاشلة تبديلاً لوصلة رمزية، لا استعادة نسخة احتياطية.
خط النشر ناجح حين يصبح النشر مملّاً: إيداع على main، وشريط أخضر، وفحص صحة يمرّ. لا رفع في آخر الليل، ولا سؤال عن آخر من لمس الخادم.
DevOps
الـ PSA يحدد اليوم والساعة قبل أن يوجد الإصلاح. ذلك وقت للتحقق من الإصدارات واختبار الـ restore وحجز الموعد، لا وقت لقراءة الـ advisory.
الأمن
4 ثغرات في NestJS خلال 7 أشهر، وكلها ثغرة واحدة: الـ matcher والـ handler اختلفا على العنوان نفسه. مكان التحقق هو حيث تُقرأ البيانات.
SEO
يدمج Next.js الـ metadata مفتاحاً مفتاحاً، فالـ route الذي يغفل alternates يرث canonical الخاص بالـ layout. وعندنا كان يشير بكل صفحة إلى الرئيسية.