واتسابدمشقالسبت – الخميس·10:00 ص – 7:00 م

من الرفع اليدوي إلى النشر الآلي — أول خط CI/CD لمشروعك

أربع مراحل، ومجلد إصدار لكل نشرة، ووصلة رمزية تتحوّل في لحظة. ملف واحد يجعل جواب "ما الذي يعمل الآن؟" رقم إيداع محدّداً.

DevOpsنُشر في 3 دقائق قراءة

ما الذي يكسره الرفع اليدوي فعلاً

مشكلة السحب والإفلات عبر FTP ليست البطء. المشكلة أن الخادم يصبح مصدر الحقيقة بدل المستودع. تعديل صغير "مؤقت" على ملف مباشرة على الخادم، ثم رفعة تالية من جهازك تمحوه دون أن يلاحظ أحد. ملفّان مختلفان بين جهازك وجهاز زميلك. رفعة انقطعت في منتصفها وتركت الموقع بنصف نسخة. ولا سجلّ يقول من غيّر ماذا ومتى، فحين ينكسر شيء لا يوجد ما تعود إليه.

الهدف من أول خط نشر ليس الأتمتة بحد ذاتها، بل أن تصبح الإجابة على سؤال "ما الذي يعمل على الخادم الآن؟" رقم إيداع (commit) واحداً محدّداً.

المراحل الأربع

أي خط نشر، مهما كبر، هو أربع مراحل:

  1. بناء — تثبيت الاعتماديات وتوليد الأصول. مرة واحدة، لا مرة في كل بيئة.
  2. اختبار — يجب أن يفشل هنا، لا على الخادم.
  3. نشر — نقل نتيجة البناء إلى الخادم.
  4. ما بعد النشر — ترحيلات قاعدة البيانات، مسح الذاكرة، فحص صحة.

ابنِها بهذا الترتيب وأضف إليها لاحقاً. خط نشر ينقل الملفات فقط أفضل بمرّات من عدم وجود خط نشر، وأسهل في الفهم من خط مثالي لا تعرف كيف تُصلحه.

نشر ذرّي بدل الكتابة فوق الملفات

rsync مباشرة فوق public_html يعني أن الموقع، لثوانٍ، خليط من نسختين. البديل هو مجلد إصدار لكل نشرة ووصلة رمزية تتحوّل في لحظة واحدة:

~/app/
  releases/9f21c4a/     ← نتيجة البناء
  releases/3c07e18/     ← الإصدار السابق، محفوظ للتراجع
  shared/.env           ← أسرار خارج المستودع
  shared/uploads/       ← ملفات المستخدمين
  current -> releases/9f21c4a

وتصبح النشرة نفسها سطراً واحداً: ln -sfn. والتراجع سطراً واحداً أيضاً، وهو نفس السطر مع اسم الإصدار السابق. اجعل public_html وصلة إلى current، أو وجّه جذر المستند إليها إن كانت الاستضافة تسمح.

أول ملف

yaml
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/health

ssh-keyscan ليس تفصيلاً تجميلياً: بدونه إمّا يتوقف SSH منتظراً تأكيداً لا يأتي، وإمّا تعطّل التحقق من مفتاح المضيف وتقبل أي خادم يجيب.

الأسرار

لا شيء حسّاس داخل المستودع، ولا حتى في مستودع خاص. المتغيّرات الحسّاسة في أسرار المستودع، والملف .env يعيش في shared/ على الخادم ويُوصَل إلى كل إصدار جديد. ولّد مفتاح نشر مخصّصاً لهذا الغرض وحده:

ssh-keygen -t ed25519 -C 'deploy@ci' -f deploy_key -N ''

العام في ~/.ssh/authorized_keys على الخادم، والخاص في أسرار المستودع. مفتاح واحد لكل مشروع ولكل بيئة، حتى يكون سحبه رخيصاً.

وإن لم يكن الحساب يفتح SSH

بعض حسابات الاستضافة المشتركة لا تمنح وصول 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، وشريط أخضر، وفحص صحة يمرّ. لا رفع في آخر الليل، ولا سؤال عن آخر من لمس الخادم.

من الرفع اليدوي إلى النشر الآلي — أول خط CI/CD لمشروعك · Qasioun Cloud