اكتشف كيف تحول نشر الكود من كابوس يدوي إلى عملية سلسة تلقائية في دقائق. دليل عملي خطوة بخطوة لبناء CI/CD من الصفر باستخدام GitHub Actions وDocker، حتى لو كنت تعمل بمفردك.
تخيل أنك تضغط زراً واحداً في نهاية يوم العمل، ويبدأ السيرفر تلقائياً في تحديث نفسه، يختبر الكود الجديد، وينشره على الإنتاج دون أن تلمس لوحة المفاتيح مرة أخرى. هذا ليس حلماً - إنه واقع CI/CD، لكن معظم المطورين الفرديين يخافون الاقتراب منه لأنهم يعتقدون أنه معقد أو مخصص للفرق الكبيرة فقط. الحقيقة؟ يمكنك بناء خط أنابيب نشر كامل في أقل من ساعة، حتى لو كنت تعمل على مشروع شخصي صغير. المشكلة الحقيقية ليست في التعقيد التقني، بل في العقلية: معظمنا اعتاد على النشر اليدوي لدرجة أنه لا يلاحظ كم من الوقت يضيع في تكرار نفس الخطوات كل مرة - تجميع الكود، تشغيل الاختبارات، نقل الملفات، إعادة تشغيل الخدمات. هذه العمليات ليست مجرد مضيعة للوقت، بل هي مصدر رئيسي للأخطاء البشرية التي قد تسبب انهيار التطبيق في أسوأ لحظة ممكنة.
في هذا الدليل، لن نتحدث عن نظريات CI/CD الجافة التي تقرأها في الوثائق الرسمية. بدلاً من ذلك، سنبني معاً خط أنابيب نشر عملي من الصفر، باستخدام أدوات حقيقية مثل GitHub Actions وDocker، مع شرح كل خطوة بما يحدث خلف الكواليس في الذاكرة والمعالج. ستتعلم كيف تحول عملية النشر من مصدر للتوتر إلى شيء يحدث تلقائياً في الخلفية بينما تشرب قهوتك. وسأريك كيف تتجنب الفخاخ الشائعة التي يقع فيها حتى المطورون المحترفون عند بناء خطوط الأنابيب لأول مرة.
عندما تعمل بمفردك على مشروع، قد تعتقد أن CI/CD مبالغة أو شيء ستضيفه لاحقاً. لكن الحقيقة هي أن المطور الفردي هو أكثر من يحتاج إليه. لماذا؟ لأنك لا تملك فريقاً يدعمك عندما ترتكب خطأً. كل خطأ في النشر يعني توقف المشروع، وربما فقدان مستخدمين. فكر في الأمر: كم مرة نسيت تشغيل اختبارات الوحدة قبل النشر؟ كم مرة نشرت كوداً يعمل على جهازك المحلي لكنه يفشل على السيرفر بسبب اختلاف الإصدارات؟ كم مرة فقدت ساعات في تتبع خطأ بسيط كان يمكن اكتشافه تلقائياً؟ هذه ليست مجرد إزعاجات - إنها تهديدات حقيقية لاستمرارية مشروعك.
في شركة ناشئة عملت بها، كان لدينا مطور فردي يبني منتجاً بمفرده. كان ينشر الكود يدوياً عبر FTP كل ليلة بعد العمل. ذات ليلة، نسي تحديث ملف التكوين، وأدى ذلك إلى توقف الخدمة لمدة 8 ساعات كاملة. ليس لأن الكود كان سيئاً، بل لأن العملية كانت يدوية. بعد ذلك، بنينا خط أنابيب CI/CD بسيط استغرق 45 دقيقة فقط، وحل المشكلة للأبد. النقطة هنا ليست في حجم الفريق، بل في اتساق العملية. CI/CD ليس أداة للفرق الكبيرة - إنه نظام حماية للمطور الفردي ضد أخطائه البشرية.
لنبدأ ببناء خط أنابيب نشر عملي باستخدام GitHub Actions، لأنه مجاني وسهل الإعداد ولا يتطلب بنية تحتية خاصة. سنفترض أنك تعمل على تطبيق Node.js بسيط، لكن المفهوم ينطبق على أي لغة أو إطار عمل. الفكرة الأساسية هي تقسيم عملية النشر إلى مراحل واضحة: البناء، الاختبار، النشر. كل مرحلة يجب أن تكون مستقلة وقابلة للتكرار، بحيث إذا فشلت إحداها، يتوقف الخط الأنابيب بأكمله دون التأثير على البيئة الإنتاجية.
أول خطوة هي إعداد بيئة العمل. ستحتاج إلى مستودع GitHub، ونظام تشغيل اختبارات، وأداة لبناء الكود. بالنسبة لتطبيق Node.js، سنستخدم npm scripts لتشغيل الاختبارات والبناء. لكن قبل أن نكتب أي كود، دعنا نفهم ما يحدث خلف الكواليس: عندما تضغط على زر الدمج في GitHub، يبدأ خادم GitHub في تشغيل آلة افتراضية جديدة (VM) مخصصة لهذه العملية فقط. هذه الآلة تأتي بنظام تشغيل نظيف (عادة Ubuntu) وتحتوي فقط على الأدوات الأساسية. مهمتك هي إعداد هذه الآلة لتشغيل الكود الخاص بك، وهذا يعني تثبيت جميع التبعيات اللازمة.
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build project
run: npm run build
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying to production server..."
# هنا ستضيف أوامر النشر الفعلية، مثل:
# scp -r dist user@server:/path/to/app
# ssh user@server "cd /path/to/app && pm2 restart app"
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SERVER_IP: ${{ secrets.SERVER_IP }}هذا الملف البسيط يفعل الكثير خلف الكواليس. أولاً، يحدد متى يجب تشغيل الخط الأنابيب: عند الدفع إلى الفرع الرئيسي أو عند فتح طلب سحب عليه. ثم يحدد مرحلتين: البناء والنشر. المرحلة الأولى (build) تقوم بتثبيت التبعيات، تشغيل الاختبارات، وبناء المشروع. إذا فشلت أي خطوة هنا، يتوقف الخط الأنابيب بأكمله. المرحلة الثانية (deploy) تعتمد على نجاح المرحلة الأولى، وتستخدم secrets مخزنة في GitHub لتسجيل الدخول إلى السيرفر ونشر الكود. لاحظ أننا استخدمنا npm ci بدلاً من npm install - هذا أمر مهم لأنه يضمن تثبيت نفس الإصدارات بالضبط كما هي محددة في ملف package-lock.json، مما يزيل أي اختلافات بين بيئات التطوير والإنتاج.
إحدى أكبر الأخطاء التي يرتكبها المطورون عند إعداد CI/CD هي تخزين المعلومات الحساسة مثل كلمات المرور ومفاتيح SSH مباشرة في ملفات التكوين. هذا خطأ أمني كبير، لأن أي شخص لديه وصول إلى المستودع يمكنه رؤية هذه المعلومات. بدلاً من ذلك، يستخدم GitHub نظام Secrets لتخزين هذه المعلومات بشكل آمن. Secrets هي متغيرات بيئة مشفرة لا يمكن قراءتها إلا من داخل الخط الأنابيب، ولا تظهر في سجلات التنفيذ أو في واجهة المستخدم.
لإعداد Secrets، انتقل إلى إعدادات المستودع في GitHub، ثم إلى Secrets and variables > Actions. هنا يمكنك إضافة متغيرات جديدة مثل SSH_PRIVATE_KEY وSERVER_IP. عند تشغيل الخط الأنابيب، يمكنك الوصول إلى هذه المتغيرات باستخدام الصيغة ${{ secrets.NAME }}. لكن هناك نقطة مهمة يجب فهمها: Secrets لا تُمرر تلقائياً إلى جميع الخطوات. إذا كنت تستخدم أمراً مثل ssh، يجب عليك أولاً إعداد مفتاح SSH الخاص باستخدام أمر مثل:
# داخل خطوة النشر في ملف deploy.yml
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan $SERVER_IP >> ~/.ssh/known_hostsهذا الكود يقوم بإنشاء مجلد .ssh إذا لم يكن موجوداً، ثم يكتب مفتاح SSH الخاص إلى ملف id_rsa، ويضبط الأذونات المناسبة. ثم يستخدم ssh-keyscan لإضافة بصمة السيرفر إلى known_hosts، مما يمنع طلب تأكيد عند الاتصال لأول مرة. هذه الخطوة مهمة لأنها تضمن أن الاتصال بـ SSH لن يتوقف منتظراً إدخال المستخدم، وهو أمر ضروري في بيئة آلية مثل CI/CD.
حتى الآن، افترضنا أنك تنشر الكود مباشرة إلى السيرفر. لكن هذا النهج له مشاكل: ماذا لو كان السيرفر يستخدم إصداراً مختلفاً من Node.js؟ ماذا لو كانت هناك تبعيات نظام غير مثبتة؟ ماذا لو كنت تريد تشغيل خدمات متعددة على نفس السيرفر؟ هنا يأتي دور Docker. باستخدام Docker، يمكنك تغليف التطبيق وكل تبعياته في حاوية واحدة، مما يضمن أنه سيعمل بنفس الطريقة على أي جهاز، سواء كان جهازك المحلي أو سيرفر الإنتاج.
أول خطوة هي كتابة Dockerfile لتطبيقك. هذا الملف يحدد كيف سيتم بناء صورة Docker الخاصة بالتطبيق. إليك مثال عملي لتطبيق Node.js:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
# نسخ ملفات التبعيات أولاً لتحسين التخزين المؤقت
COPY package*.json ./
RUN npm ci --production
# نسخ باقي الملفات
COPY . .
# بناء التطبيق إذا كان مطلوباً
RUN npm run build
# تحديد المنفذ الذي يستمع إليه التطبيق
EXPOSE 3000
# الأمر الذي سيُشغل عند تشغيل الحاوية
CMD ["node", "dist/index.js"]هذا Dockerfile يبدأ من صورة Node.js الرسمية المبنية على Alpine Linux، وهي صورة خفيفة الوزن مثالية للإنتاج. لاحظ أننا نسخنا ملفات package.json وpackage-lock.json أولاً، ثم قمنا بتثبيت التبعيات باستخدام npm ci. هذا أمر مهم لتحسين التخزين المؤقت: إذا لم تتغير ملفات التبعيات، فلن يعيد Docker بناء هذه الطبقة عند تغيير الكود، مما يسرع عملية البناء بشكل كبير.
الآن، يمكننا تعديل ملف CI/CD لتشمل بناء صورة Docker ودفعها إلى سجل حاويات مثل Docker Hub أو GitHub Container Registry. إليك كيف يمكن تعديل ملف deploy.yml:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: yourusername/yourapp:latest,yourusername/yourapp:${{ github.sha }}
- name: Deploy to production
run: |
ssh user@$SERVER_IP "docker pull yourusername/yourapp:latest && docker stop yourapp || true && docker rm yourapp || true && docker run -d --name yourapp -p 3000:3000 yourusername/yourapp:latest"هذا الكود يقوم بتسجيل الدخول إلى Docker Hub، ثم يبني صورة Docker ويدفعها إلى السجل. لاحظ أننا استخدمنا علامتين للصورة: latest للإشارة إلى أحدث إصدار، و${{ github.sha }} للإشارة إلى الإصدار المحدد باستخدام تجزئة الالتزام. هذا يسمح لنا بالعودة إلى إصدار معين إذا لزم الأمر. ثم نقوم بتسجيل الدخول إلى السيرفر عبر SSH، وسحب الصورة الجديدة، وإيقاف وإزالة الحاوية القديمة، وتشغيل الحاوية الجديدة. هذه الطريقة تضمن أن عملية النشر لا تستغرق أكثر من بضع ثوانٍ، وأن التطبيق يعمل دائماً في بيئة نظيفة ومعزولة.
بناء خط أنابيب CI/CD لأول مرة قد يكون محبطاً بسبب الأخطاء الصغيرة التي قد تسبب فشل العملية بأكملها. أحد أكثر الأخطاء شيوعاً هو نسيان تثبيت التبعيات اللازمة في بيئة CI. على سبيل المثال، إذا كان تطبيقك يستخدم قاعدة بيانات PostgreSQL، وتحتاج إلى تشغيل اختبارات التكامل، يجب عليك تثبيت PostgreSQL في بيئة CI قبل تشغيل الاختبارات. لكن تثبيت قاعدة بيانات كاملة في كل مرة قد يكون بطيئاً ومكلفاً. الحل هو استخدام خدمات مثل GitHub Actions services أو Docker Compose لتشغيل قاعدة بيانات مؤقتة أثناء الاختبارات.
خطأ شائع آخر هو عدم التعامل مع الفشل بشكل صحيح. على سبيل المثال، إذا فشلت خطوة الاختبار، يجب ألا يستمر الخط الأنابيب في النشر. لكن ماذا لو أردت تشغيل بعض الخطوات حتى في حالة الفشل، مثل إرسال إشعار؟ هنا يأتي دور الكلمة المفتاحية if في GitHub Actions. يمكنك استخدام if: always() لتشغيل خطوة بغض النظر عن حالة الفشل. إليك مثال:
- name: Notify on failure
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":"❌ Build failed for ${{ github.repository }}!"}' ${{ secrets.SLACK_WEBHOOK_URL }}هذا الكود يرسل إشعاراً إلى Slack إذا فشلت أي خطوة سابقة في الخط الأنابيب. يمكنك أيضاً استخدام if: success() لإرسال إشعار عند النجاح. نقطة مهمة أخرى هي التعامل مع الوقت: بعض الخطوات قد تستغرق وقتاً طويلاً، مثل بناء صورة Docker كبيرة. يمكنك استخدام الكلمة المفتاحية timeout-minutes لتحديد الحد الأقصى للوقت المسموح به لخطوة معينة، مما يمنع الخط الأنابيب من التعليق لساعات.
بناء خط أنابيب CI/CD ليس نهاية القصة - يجب عليك مراقبته وتحسينه باستمرار. GitHub Actions يوفر سجلات مفصلة لكل تشغيل، بما في ذلك الوقت المستغرق لكل خطوة. استخدم هذه المعلومات لتحديد الاختناقات في العملية. على سبيل المثال، إذا كانت خطوة تثبيت التبعيات تستغرق 5 دقائق، ربما يمكنك استخدام التخزين المؤقت لتسريعها. إليك كيف يمكنك إضافة التخزين المؤقت لتبعيات Node.js:
- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-هذا الكود يخزن مجلد .npm في ذاكرة التخزين المؤقت بين عمليات التشغيل. إذا لم تتغير ملفات package-lock.json، سيتم استعادة التبعيات من التخزين المؤقت بدلاً من تنزيلها مرة أخرى، مما يوفر وقتاً كبيراً. يمكنك تطبيق نفس المبدأ على أي تبعيات أخرى، مثل مكتبات Python أو حزم Go.
نقطة أخرى مهمة هي تحسين وقت البناء. إذا كان تطبيقك كبيراً، ربما يمكنك تقسيم الاختبارات إلى مجموعات وتشغيلها بالتوازي. GitHub Actions يدعم تشغيل الوظائف بالتوازي باستخدام الكلمة المفتاحية strategy. إليك مثال لتشغيل اختبارات الوحدة واختبارات التكامل بالتوازي:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-type: [unit, integration]
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test:${{ matrix.test-type }}هذا الكود سيخلق وظيفتين منفصلتين، واحدة لاختبارات الوحدة والأخرى لاختبارات التكامل، وسيتم تشغيلهما بالتوازي. هذا يقلل من الوقت الإجمالي لتشغيل الاختبارات بشكل كبير، خاصة إذا كانت اختبارات التكامل بطيئة.
إذا أخذت شيئاً واحداً فقط من هذا الدليل، فليكن هذا: ابدأ صغيراً، ثم طور. لا تحاول بناء خط أنابيب مثالي من اليوم الأول. ابدأ بخط أنابيب بسيط يقوم بتشغيل الاختبارات فقط، ثم أضف خطوات تدريجياً. استخدم Docker منذ البداية، حتى لو كان تطبيقك صغيراً - ستوفر على نفسك ساعات من الصداع لاحقاً. وأخيراً، تذكر أن الهدف من CI/CD ليس فقط أتمتة النشر، بل بناء عملية يمكن الاعتماد عليها وتكرارها دون خوف. عندما تصل إلى النقطة التي يمكنك فيها النشر بضغطة زر دون القلق من انهيار التطبيق، ستدرك أن هذه الساعات التي قضيتها في إعداد الخط الأنابيب كانت أفضل استثمار في وقتك كمطور فردي.